Skip to content

Commit fff2ddd

Browse files
glitch003adamdossa
authored andcommitted
Lockup Volume Restrictions (#290)
* first commit. contracts compile, but need to write tests * basic tests pass * more tests * more tests * more tests * better tests * more coverage * missed one branch of coverage. this should fix it * put back old package-lock.json * comment fix * added one more test * make transfer fail if lockup startTime hasn't passed yet * merged dev-1.5.0 latest in and fixed tests to work with that * put back old package-lock.json * add newline to end of package-lock.json to clean up PR * code in place to track balances per lockup. but i don't think this will work long term so moving it into this branch * made changes requested * changed to store withdrawn balances inside each lockup * rename test * updated before hook to use new stuff so that tests will run * Added a test case * make releaseFrequencySeconds a bit longer in a test so that the tests are happier on travis * Update VolumeRestrictionTransferManager.sol * Update w_volume_restriction_transfer_manager.js * fixed to work with latest dev-1.5.0 changes * minor fixes
1 parent 7992f9d commit fff2ddd

File tree

7 files changed

+3830
-3314
lines changed

7 files changed

+3830
-3314
lines changed

contracts/interfaces/ISecurityToken.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,4 +280,10 @@ interface ISecurityToken {
280280
* @return bool success
281281
*/
282282
function transferFromWithData(address _from, address _to, uint256 _value, bytes _data) external returns(bool);
283+
284+
/**
285+
* @notice Provide the granularity of the token
286+
* @return uint256
287+
*/
288+
function granularity() external view returns(uint256);
283289
}
Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
pragma solidity ^0.4.24;
2+
3+
import "./ITransferManager.sol";
4+
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
5+
6+
7+
contract VolumeRestrictionTransferManager is ITransferManager {
8+
9+
using SafeMath for uint256;
10+
11+
// permission definition
12+
bytes32 public constant ADMIN = "ADMIN";
13+
14+
// a per-user lockup
15+
struct LockUp {
16+
uint lockUpPeriodSeconds; // total period of lockup (seconds)
17+
uint releaseFrequencySeconds; // how often to release a tranche of tokens (seconds)
18+
uint startTime; // when this lockup starts (seconds)
19+
uint totalAmount; // total amount of locked up tokens
20+
uint alreadyWithdrawn; // amount already withdrawn for this lockup
21+
}
22+
23+
// maps user addresses to an array of lockups for that user
24+
mapping (address => LockUp[]) internal lockUps;
25+
26+
event AddNewLockUp(
27+
address indexed userAddress,
28+
uint lockUpPeriodSeconds,
29+
uint releaseFrequencySeconds,
30+
uint startTime,
31+
uint totalAmount,
32+
uint indexed addedIndex
33+
);
34+
35+
event RemoveLockUp(
36+
address indexed userAddress,
37+
uint lockUpPeriodSeconds,
38+
uint releaseFrequencySeconds,
39+
uint startTime,
40+
uint totalAmount,
41+
uint indexed removedIndex
42+
);
43+
44+
event ModifyLockUp(
45+
address indexed userAddress,
46+
uint lockUpPeriodSeconds,
47+
uint releaseFrequencySeconds,
48+
uint startTime,
49+
uint totalAmount,
50+
uint indexed modifiedIndex
51+
);
52+
53+
/**
54+
* @notice Constructor
55+
* @param _securityToken Address of the security token
56+
* @param _polyAddress Address of the polytoken
57+
*/
58+
constructor (address _securityToken, address _polyAddress)
59+
public
60+
Module(_securityToken, _polyAddress)
61+
{
62+
}
63+
64+
65+
/** @notice Used to verify the transfer transaction and prevent locked up tokens from being transferred
66+
* @param _from Address of the sender
67+
* @param _amount The amount of tokens to transfer
68+
* @param _isTransfer Whether or not this is an actual transfer or just a test to see if the tokens would be transferrable
69+
*/
70+
function verifyTransfer(address _from, address /* _to*/, uint256 _amount, bytes /* _data */, bool _isTransfer) public returns(Result) {
71+
// 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
72+
if (!paused && _from != address(0) && lockUps[_from].length != 0) {
73+
// check if this transfer is valid
74+
return _checkIfValidTransfer(_from, _amount, _isTransfer);
75+
}
76+
return Result.NA;
77+
}
78+
79+
/**
80+
* @notice Lets the admin create a volume restriction lockup for a given address.
81+
* @param userAddress Address of the user whose tokens should be locked up
82+
* @param lockUpPeriodSeconds Total period of lockup (seconds)
83+
* @param releaseFrequencySeconds How often to release a tranche of tokens (seconds)
84+
* @param startTime When this lockup starts (seconds)
85+
* @param totalAmount Total amount of locked up tokens
86+
*/
87+
function addLockUp(address userAddress, uint lockUpPeriodSeconds, uint releaseFrequencySeconds, uint startTime, uint totalAmount) public withPerm(ADMIN) {
88+
89+
_checkLockUpParams(lockUpPeriodSeconds, releaseFrequencySeconds, totalAmount);
90+
91+
// if a startTime of 0 is passed in, then start now.
92+
if (startTime == 0) {
93+
startTime = now;
94+
}
95+
96+
lockUps[userAddress].push(LockUp(lockUpPeriodSeconds, releaseFrequencySeconds, startTime, totalAmount, 0));
97+
98+
emit AddNewLockUp(
99+
userAddress,
100+
lockUpPeriodSeconds,
101+
releaseFrequencySeconds,
102+
startTime,
103+
totalAmount,
104+
lockUps[userAddress].length - 1
105+
);
106+
}
107+
108+
/**
109+
* @notice Lets the admin create multiple volume restriction lockups for multiple given addresses.
110+
* @param userAddresses Array of address of the user whose tokens should be locked up
111+
* @param lockUpPeriodsSeconds Array of total periods of lockup (seconds)
112+
* @param releaseFrequenciesSeconds Array of how often to release a tranche of tokens (seconds)
113+
* @param startTimes Array of When this lockup starts (seconds)
114+
* @param totalAmounts Array of total amount of locked up tokens
115+
*/
116+
function addLockUpMulti(address[] userAddresses, uint[] lockUpPeriodsSeconds, uint[] releaseFrequenciesSeconds, uint[] startTimes, uint[] totalAmounts) external withPerm(ADMIN) {
117+
118+
// make sure input params are sane
119+
require(
120+
userAddresses.length == lockUpPeriodsSeconds.length &&
121+
userAddresses.length == releaseFrequenciesSeconds.length &&
122+
userAddresses.length == startTimes.length &&
123+
userAddresses.length == totalAmounts.length,
124+
"Input array length mis-match"
125+
);
126+
127+
for (uint i = 0; i < userAddresses.length; i++) {
128+
addLockUp(userAddresses[i], lockUpPeriodsSeconds[i], releaseFrequenciesSeconds[i], startTimes[i], totalAmounts[i]);
129+
}
130+
131+
}
132+
133+
/**
134+
* @notice Lets the admin remove a user's lock up
135+
* @param userAddress Address of the user whose tokens are locked up
136+
* @param lockUpIndex The index of the LockUp to remove for the given userAddress
137+
*/
138+
function removeLockUp(address userAddress, uint lockUpIndex) public withPerm(ADMIN) {
139+
LockUp[] storage userLockUps = lockUps[userAddress];
140+
require(lockUpIndex < userLockUps.length, "Array out of bounds exception");
141+
142+
LockUp memory toRemove = userLockUps[lockUpIndex];
143+
144+
emit RemoveLockUp(
145+
userAddress,
146+
toRemove.lockUpPeriodSeconds,
147+
toRemove.releaseFrequencySeconds,
148+
toRemove.startTime,
149+
toRemove.totalAmount,
150+
lockUpIndex
151+
);
152+
153+
if (lockUpIndex < userLockUps.length - 1) {
154+
// move the last element in the array into the index that is desired to be removed.
155+
userLockUps[lockUpIndex] = userLockUps[userLockUps.length - 1];
156+
}
157+
// delete the last element
158+
userLockUps.length--;
159+
}
160+
161+
/**
162+
* @notice Lets the admin modify a volume restriction lockup for a given address.
163+
* @param userAddress Address of the user whose tokens should be locked up
164+
* @param lockUpIndex The index of the LockUp to edit for the given userAddress
165+
* @param lockUpPeriodSeconds Total period of lockup (seconds)
166+
* @param releaseFrequencySeconds How often to release a tranche of tokens (seconds)
167+
* @param startTime When this lockup starts (seconds)
168+
* @param totalAmount Total amount of locked up tokens
169+
*/
170+
function modifyLockUp(address userAddress, uint lockUpIndex, uint lockUpPeriodSeconds, uint releaseFrequencySeconds, uint startTime, uint totalAmount) public withPerm(ADMIN) {
171+
require(lockUpIndex < lockUps[userAddress].length, "Array out of bounds exception");
172+
173+
// if a startTime of 0 is passed in, then start now.
174+
if (startTime == 0) {
175+
startTime = now;
176+
}
177+
178+
_checkLockUpParams(lockUpPeriodSeconds, releaseFrequencySeconds, totalAmount);
179+
180+
// Get the lockup from the master list and edit it
181+
lockUps[userAddress][lockUpIndex] = LockUp(
182+
lockUpPeriodSeconds,
183+
releaseFrequencySeconds,
184+
startTime,
185+
totalAmount,
186+
lockUps[userAddress][lockUpIndex].alreadyWithdrawn
187+
);
188+
189+
emit ModifyLockUp(
190+
userAddress,
191+
lockUpPeriodSeconds,
192+
releaseFrequencySeconds,
193+
startTime,
194+
totalAmount,
195+
lockUpIndex
196+
);
197+
}
198+
199+
/**
200+
* @notice Get the length of the lockups array for a specific user address
201+
* @param userAddress Address of the user whose tokens should be locked up
202+
*/
203+
function getLockUpsLength(address userAddress) public view returns (uint) {
204+
return lockUps[userAddress].length;
205+
}
206+
207+
/**
208+
* @notice Get a specific element in a user's lockups array given the user's address and the element index
209+
* @param userAddress Address of the user whose tokens should be locked up
210+
* @param lockUpIndex The index of the LockUp to edit for the given userAddress
211+
*/
212+
function getLockUp(address userAddress, uint lockUpIndex) public view returns (uint lockUpPeriodSeconds, uint releaseFrequencySeconds, uint startTime, uint totalAmount, uint alreadyWithdrawn) {
213+
require(lockUpIndex < lockUps[userAddress].length, "Array out of bounds exception");
214+
LockUp storage userLockUp = lockUps[userAddress][lockUpIndex];
215+
return (
216+
userLockUp.lockUpPeriodSeconds,
217+
userLockUp.releaseFrequencySeconds,
218+
userLockUp.startTime,
219+
userLockUp.totalAmount,
220+
userLockUp.alreadyWithdrawn
221+
);
222+
}
223+
224+
/**
225+
* @notice This function returns the signature of configure function
226+
*/
227+
function getInitFunction() public pure returns (bytes4) {
228+
return bytes4(0);
229+
}
230+
231+
/**
232+
* @notice Return the permissions flag that are associated with Percentage transfer Manager
233+
*/
234+
function getPermissions() public view returns(bytes32[]) {
235+
bytes32[] memory allPermissions = new bytes32[](1);
236+
allPermissions[0] = ADMIN;
237+
return allPermissions;
238+
}
239+
240+
/**
241+
* @notice Takes a userAddress as input, and returns a uint that represents the number of tokens allowed to be withdrawn right now
242+
* @param userAddress Address of the user whose lock ups should be checked
243+
*/
244+
function _checkIfValidTransfer(address userAddress, uint amount, bool isTransfer) internal returns (Result) {
245+
// get lock up array for this user
246+
LockUp[] storage userLockUps = lockUps[userAddress];
247+
248+
// maps the index of userLockUps to the amount allowed in this transfer
249+
uint[] memory allowedAmountPerLockup = new uint[](userLockUps.length);
250+
251+
uint[3] memory tokenSums = [
252+
uint256(0), // allowed amount right now
253+
uint256(0), // total locked up, ever
254+
uint256(0) // already withdrawn, ever
255+
];
256+
257+
// loop over the user's lock ups
258+
for (uint i = 0; i < userLockUps.length; i++) {
259+
LockUp storage aLockUp = userLockUps[i];
260+
261+
uint allowedAmountForThisLockup = 0;
262+
263+
// check if lockup has entirely passed
264+
if (now >= aLockUp.startTime.add(aLockUp.lockUpPeriodSeconds)) {
265+
// lockup has passed, or not started yet. allow all.
266+
allowedAmountForThisLockup = aLockUp.totalAmount.sub(aLockUp.alreadyWithdrawn);
267+
} else if (now >= aLockUp.startTime) {
268+
// lockup is active. calculate how many to allow to be withdrawn right now
269+
// calculate how many periods have elapsed already
270+
uint elapsedPeriods = (now.sub(aLockUp.startTime)).div(aLockUp.releaseFrequencySeconds);
271+
// calculate the total number of periods, overall
272+
uint totalPeriods = aLockUp.lockUpPeriodSeconds.div(aLockUp.releaseFrequencySeconds);
273+
// calculate how much should be released per period
274+
uint amountPerPeriod = aLockUp.totalAmount.div(totalPeriods);
275+
// calculate the number of tokens that should be released,
276+
// multiplied by the number of periods that have elapsed already
277+
// and add it to the total tokenSums[0]
278+
allowedAmountForThisLockup = amountPerPeriod.mul(elapsedPeriods).sub(aLockUp.alreadyWithdrawn);
279+
280+
}
281+
// tokenSums[0] is allowed sum
282+
tokenSums[0] = tokenSums[0].add(allowedAmountForThisLockup);
283+
// tokenSums[1] is total locked up
284+
tokenSums[1] = tokenSums[1].add(aLockUp.totalAmount);
285+
// tokenSums[2] is total already withdrawn
286+
tokenSums[2] = tokenSums[2].add(aLockUp.alreadyWithdrawn);
287+
288+
allowedAmountPerLockup[i] = allowedAmountForThisLockup;
289+
}
290+
291+
// tokenSums[0] is allowed sum
292+
if (amount <= tokenSums[0]) {
293+
// transfer is valid and will succeed.
294+
if (!isTransfer) {
295+
// if this isn't a real transfer, don't subtract the withdrawn amounts from the lockups. it's a "read only" txn
296+
return Result.VALID;
297+
}
298+
299+
// 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
300+
require(msg.sender == securityToken, "Sender is not securityToken");
301+
302+
// subtract amounts so they are now known to be withdrawen
303+
for (i = 0; i < userLockUps.length; i++) {
304+
aLockUp = userLockUps[i];
305+
306+
// tokenSums[0] is allowed sum
307+
if (allowedAmountPerLockup[i] >= tokenSums[0]) {
308+
aLockUp.alreadyWithdrawn = aLockUp.alreadyWithdrawn.add(tokenSums[0]);
309+
// we withdrew the entire tokenSums[0] from the lockup. We are done.
310+
break;
311+
} else {
312+
// we have to split the tokenSums[0] across mutiple lockUps
313+
aLockUp.alreadyWithdrawn = aLockUp.alreadyWithdrawn.add(allowedAmountPerLockup[i]);
314+
// subtract the amount withdrawn from this lockup
315+
tokenSums[0] = tokenSums[0].sub(allowedAmountPerLockup[i]);
316+
}
317+
318+
}
319+
return Result.VALID;
320+
}
321+
322+
return _checkIfUnlockedTokenTransferIsPossible(userAddress, amount, tokenSums[1], tokenSums[2]);
323+
}
324+
325+
function _checkIfUnlockedTokenTransferIsPossible(address userAddress, uint amount, uint totalSum, uint alreadyWithdrawnSum) internal view returns (Result) {
326+
// 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
327+
uint currentUserBalance = ISecurityToken(securityToken).balanceOf(userAddress);
328+
uint stillLockedAmount = totalSum.sub(alreadyWithdrawnSum);
329+
if (currentUserBalance >= stillLockedAmount && amount <= currentUserBalance.sub(stillLockedAmount)) {
330+
// the user has more tokens in their balance than are actually locked up. they should be allowed to withdraw the difference
331+
return Result.VALID;
332+
}
333+
return Result.INVALID;
334+
}
335+
336+
337+
/**
338+
* @notice Parameter checking function for creating or editing a lockup. This function will cause an exception if any of the parameters are bad.
339+
* @param lockUpPeriodSeconds Total period of lockup (seconds)
340+
* @param releaseFrequencySeconds How often to release a tranche of tokens (seconds)
341+
* @param totalAmount Total amount of locked up tokens
342+
*/
343+
function _checkLockUpParams(uint lockUpPeriodSeconds, uint releaseFrequencySeconds, uint totalAmount) internal view {
344+
require(lockUpPeriodSeconds != 0, "lockUpPeriodSeconds cannot be zero");
345+
require(releaseFrequencySeconds != 0, "releaseFrequencySeconds cannot be zero");
346+
require(totalAmount != 0, "totalAmount cannot be zero");
347+
348+
// check that the total amount to be released isn't too granular
349+
require(
350+
totalAmount % ISecurityToken(securityToken).granularity() == 0,
351+
"The total amount to be released is more granular than allowed by the token"
352+
);
353+
354+
// check that releaseFrequencySeconds evenly divides lockUpPeriodSeconds
355+
require(
356+
lockUpPeriodSeconds % releaseFrequencySeconds == 0,
357+
"lockUpPeriodSeconds must be evenly divisible by releaseFrequencySeconds"
358+
);
359+
360+
// check that totalPeriods evenly divides totalAmount
361+
uint totalPeriods = lockUpPeriodSeconds.div(releaseFrequencySeconds);
362+
require(
363+
totalAmount % totalPeriods == 0,
364+
"The total amount being locked up must be evenly divisible by the number of total periods"
365+
);
366+
367+
// make sure the amount to be released per period is not too granular for the token
368+
uint amountPerPeriod = totalAmount.div(totalPeriods);
369+
require(
370+
amountPerPeriod % ISecurityToken(securityToken).granularity() == 0,
371+
"The amount to be released per period is more granular than allowed by the token"
372+
);
373+
}
374+
}

0 commit comments

Comments
 (0)