Skip to content

Commit

Permalink
Merge pull request #4 from deconet/volume-restriction-transfer-manage…
Browse files Browse the repository at this point in the history
…r-with-internal-withdrawn-balances

Volume restriction transfer manager with internal withdrawn balances
  • Loading branch information
glitch003 authored Oct 4, 2018
2 parents 64b627f + d935adf commit 5f725af
Show file tree
Hide file tree
Showing 2 changed files with 256 additions and 38 deletions.
156 changes: 119 additions & 37 deletions contracts/modules/TransferManager/VolumeRestrictionTransferManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ contract VolumeRestrictionTransferManager is ITransferManager {
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
Expand All @@ -30,7 +31,7 @@ contract VolumeRestrictionTransferManager is ITransferManager {
uint startTime,
uint totalAmount
);

event RemoveLockUp(
address indexed userAddress,
uint lockUpPeriodSeconds,
Expand Down Expand Up @@ -62,16 +63,13 @@ contract VolumeRestrictionTransferManager is ITransferManager {
/** @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, bool /* _isTransfer */) public returns(Result) {
function verifyTransfer(address _from, address /* _to*/, uint256 _amount, 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) {
// get total amount allowed right now
uint allowedAmount = _currentAllowedAmount(_from);
if (_amount <= allowedAmount) {
return Result.VALID;
}
return Result.INVALID;
// check if this transfer is valid
return _checkIfValidTransfer(_from, _amount, _isTransfer);
}
return Result.NA;
}
Expand All @@ -93,7 +91,7 @@ contract VolumeRestrictionTransferManager is ITransferManager {
startTime = now;
}

lockUps[userAddress].push(LockUp(lockUpPeriodSeconds, releaseFrequencySeconds, startTime, totalAmount));
lockUps[userAddress].push(LockUp(lockUpPeriodSeconds, releaseFrequencySeconds, startTime, totalAmount, 0));

emit AddNewLockUp(
userAddress,
Expand Down Expand Up @@ -176,7 +174,13 @@ contract VolumeRestrictionTransferManager is ITransferManager {
_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] = LockUp(
lockUpPeriodSeconds,
releaseFrequencySeconds,
startTime,
totalAmount,
lockUps[userAddress][lockUpIndex].alreadyWithdrawn
);

emit ModifyLockUp(
userAddress,
Expand All @@ -200,18 +204,18 @@ contract VolumeRestrictionTransferManager is ITransferManager {
* @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) {
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.totalAmount,
userLockUp.alreadyWithdrawn
);
}


/**
* @notice This function returns the signature of configure function
*/
Expand All @@ -232,37 +236,31 @@ contract VolumeRestrictionTransferManager is ITransferManager {
* @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 _currentAllowedAmount(address userAddress) internal view returns (uint) {
function _checkIfValidTransfer(address userAddress, uint amount, bool isTransfer) internal returns (Result) {
// get lock up array for this user
LockUp[] storage userLockUps = lockUps[userAddress];

// we will return the total amount allowed for this user right now, across all their lock ups.
uint allowedSum = 0;
// maps the index of userLockUps to the amount allowed in this transfer
uint[] memory allowedAmountPerLockup = new uint[](userLockUps.length);

// save the total number of granted tokens, ever
// so that we can subtract the already withdrawn balance from the amount to be allowed to withdraw
uint totalSum = 0;
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];

// has lockup started yet?
if (now < aLockUp.startTime) {
// it has not. don't let them transfer any tokens.
continue;
}

// add total amount to totalSum
totalSum = totalSum.add(aLockUp.totalAmount);
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.
allowedSum = allowedSum.add(aLockUp.totalAmount);
} else {
// lockup is still active. calculate how many to allow to be withdrawn right now

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
Expand All @@ -271,17 +269,101 @@ contract VolumeRestrictionTransferManager is ITransferManager {
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 allowedSum
allowedSum = allowedSum.add(amountPerPeriod.mul(elapsedPeriods));
// 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;
}

// subtract already withdrawn amount
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);
uint alreadyWithdrawn = totalSum.sub(currentUserBalance);
uint allowedAmount = allowedSum.sub(alreadyWithdrawn);
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;
}

return allowedAmount;
/**
* @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 % SecurityToken(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 % SecurityToken(securityToken).granularity() == 0,
"The amount to be released per period is more granular than allowed by the token"
);
}

/**
Expand Down
Loading

0 comments on commit 5f725af

Please sign in to comment.