Skip to content
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

Lockup Volume Restrictions #290

Merged
Show file tree
Hide file tree
Changes from 16 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 Sep 25, 2018
cd6289c
basic tests pass
glitch003 Sep 26, 2018
164e247
more tests
glitch003 Sep 26, 2018
9a69ab6
more tests
glitch003 Sep 27, 2018
51a9d1f
more tests
glitch003 Sep 27, 2018
140bbf3
better tests
glitch003 Sep 27, 2018
2ecc821
more coverage
glitch003 Sep 28, 2018
8dc4a28
missed one branch of coverage. this should fix it
glitch003 Sep 28, 2018
2c28750
put back old package-lock.json
glitch003 Sep 28, 2018
02cd36e
comment fix
glitch003 Sep 28, 2018
ed0c3b9
added one more test
glitch003 Sep 28, 2018
5e844d7
Merge branch 'development-1.5.0' into volume-restriction-transfer-man…
glitch003 Sep 28, 2018
39513f4
make transfer fail if lockup startTime hasn't passed yet
glitch003 Sep 28, 2018
6421e09
Merge pull request #2 from PolymathNetwork/development-1.5.0
glitch003 Sep 28, 2018
c36217c
merged dev-1.5.0 latest in and fixed tests to work with that
glitch003 Sep 28, 2018
7395a8c
put back old package-lock.json
glitch003 Sep 28, 2018
5a2c338
add newline to end of package-lock.json to clean up PR
glitch003 Sep 28, 2018
5781936
code in place to track balances per lockup. but i don't think this w…
glitch003 Oct 2, 2018
64b627f
made changes requested
glitch003 Oct 2, 2018
b31eaf4
changed to store withdrawn balances inside each lockup
glitch003 Oct 4, 2018
d935adf
Merge branch 'volume-restriction-transfer-manager' into volume-restri…
glitch003 Oct 4, 2018
5f725af
Merge pull request #4 from deconet/volume-restriction-transfer-manage…
glitch003 Oct 4, 2018
3904fa9
Merge pull request #5 from PolymathNetwork/development-1.5.0
glitch003 Oct 4, 2018
52a3016
Merge pull request #6 from deconet/development-1.5.0
glitch003 Oct 4, 2018
c9b67ef
rename test
glitch003 Oct 4, 2018
db3e27d
updated before hook to use new stuff so that tests will run
glitch003 Oct 4, 2018
30bd69a
Added a test case
glitch003 Oct 4, 2018
7b8d302
make releaseFrequencySeconds a bit longer in a test so that the tests…
glitch003 Oct 4, 2018
1343f65
Merge branch 'development-1.5.0' into volume-restriction-transfer-man…
adamdossa Oct 4, 2018
c392a12
Update VolumeRestrictionTransferManager.sol
adamdossa Oct 4, 2018
54393b0
Update w_volume_restriction_transfer_manager.js
adamdossa Oct 4, 2018
e984447
fixed to work with latest dev-1.5.0 changes
glitch003 Oct 4, 2018
6ec89eb
minor fixes
SatyamSB Oct 5, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 241 additions & 0 deletions contracts/modules/TransferManager/VolumeRestrictionTransferManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
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
}

// maps user addresses to an array of lockups for that user
mapping (address => LockUp[]) internal lockUps;

enum LockUpOperationType { Add, Remove, Edit }

event LogModifyLockUp(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we change our event naming convention, So please remove the Log prefix from the name.
and I prefer the new event for operation type so it is easily understandable with the name.
ex-
event addNewLockUp(...)
event removeLockUP(...)
event modifyLockUp(...)

address indexed userAddress,
uint lockUpPeriodSeconds,
uint releaseFrequencySeconds,
uint startTime,
uint totalAmount,
LockUpOperationType indexed operationType
);


/**
* @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
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;
}
return Result.NA;
}

// lets the owner / admin create a volume restriction lockup.
// takes a userAddress and lock up params, and creates a lockup for the user. See the LockUp struct def for info on what each parameter actually is
function addLockUp(address userAddress, uint lockUpPeriodSeconds, uint releaseFrequencySeconds, uint startTime, uint totalAmount) public withPerm(ADMIN) {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to add some sanity checks for the input parameters

// if a startTime of 0 is passed in, then start now.
if (startTime == 0) {
startTime = now;
}

// Get an array of the user's lockups
LockUp[] storage userLockUps = lockUps[userAddress];

// create a new lock up and push it into the array of the user's lock ups
LockUp memory newLockUp;
newLockUp.lockUpPeriodSeconds = lockUpPeriodSeconds;
newLockUp.releaseFrequencySeconds = releaseFrequencySeconds;
newLockUp.startTime = startTime;
newLockUp.totalAmount = totalAmount;
userLockUps.push(newLockUp);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of the current approach, it can be re-write in a single line as
lockUps[userAddress].push(LockUP(lockUpPeriodSeconds, releaseFrequencySeconds, startTime , totalAmount))


emit LogModifyLockUp(
userAddress,
lockUpPeriodSeconds,
releaseFrequencySeconds,
startTime,
totalAmount,
LockUpOperationType.Add
);
}

// same as addLockup, but takes an array for each parameter
function addLockUpMulti(address[] userAddresses, uint[] lockUpPeriodsSeconds, uint[] releaseFrequenciesSeconds, uint[] startTimes, uint[] totalAmounts) public 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]);
}

}

// remove a user's lock up
function removeLockUp(address userAddress, uint lockUpIndex) public withPerm(ADMIN) {
LockUp[] storage userLockUps = lockUps[userAddress];
LockUp memory toRemove = userLockUps[lockUpIndex];

glitch003 marked this conversation as resolved.
Show resolved Hide resolved
emit LogModifyLockUp(
userAddress,
toRemove.lockUpPeriodSeconds,
toRemove.releaseFrequencySeconds,
toRemove.startTime,
toRemove.totalAmount,
LockUpOperationType.Remove
);

// 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
}

function editLockUp(address userAddress, uint lockUpIndex, uint lockUpPeriodSeconds, uint releaseFrequencySeconds, uint startTime, uint totalAmount) public withPerm(ADMIN) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename the function name to modifyLockUp()


// if a startTime of 0 is passed in, then start now.
if (startTime == 0) {
startTime = now;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sanity checks are missing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @satyamakgec - could you clarify this one? What kind of sanity check were you thinking?

It was my thinking that a startTime before "now" may be desired if you want to backdate someone's vesting, which I think is a common thing. And obviously a startTime after "now" could be desired if you're awarding tokens to an employee who, for example, starts working in 2 weeks or something.

// Get the lockup from the master list and edit it
LockUp storage userLockUp = lockUps[userAddress][lockUpIndex];

userLockUp.lockUpPeriodSeconds = lockUpPeriodSeconds;
userLockUp.releaseFrequencySeconds = releaseFrequencySeconds;
userLockUp.startTime = startTime;
userLockUp.totalAmount = totalAmount;

glitch003 marked this conversation as resolved.
Show resolved Hide resolved
emit LogModifyLockUp(
userAddress,
lockUpPeriodSeconds,
releaseFrequencySeconds,
startTime,
totalAmount,
LockUpOperationType.Edit
);
}

// get the length of the lockups array for a specific user
function getLockUpsLength(address userAddress) public view returns (uint) {
return lockUps[userAddress].length;
}

// get a specific element in a user's lockups array given the user's address and the element index
function getLockUp(address userAddress, uint lockUpIndex) public view returns (uint lockUpPeriodSeconds, uint releaseFrequencySeconds, uint startTime, uint totalAmount) {
LockUp storage userLockUp = lockUps[userAddress][lockUpIndex];
return (
userLockUp.lockUpPeriodSeconds,
userLockUp.releaseFrequencySeconds,
userLockUp.startTime,
userLockUp.totalAmount
);
}


/**
* @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;
}

// this takes a userAddress as input, and returns a uint that represents the number of tokens allowed to be withdrawn right now
function _currentAllowedAmount(address userAddress) internal view returns (uint) {
// 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;

// 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;

// 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);

// 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

// 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 allowedSum
allowedSum = allowedSum.add(amountPerPeriod.mul(elapsedPeriods));
}
}

// subtract already withdrawn amount
uint currentUserBalance = ISecurityToken(securityToken).balanceOf(userAddress);
glitch003 marked this conversation as resolved.
Show resolved Hide resolved
uint alreadyWithdrawn = totalSum.sub(currentUserBalance);
uint allowedAmount = allowedSum.sub(alreadyWithdrawn);

return allowedAmount;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
pragma solidity ^0.4.24;

import "./VolumeRestrictionTransferManager.sol";
import "../ModuleFactory.sol";

/**
* @title Factory for deploying ManualApprovalTransferManager module
*/
contract VolumeRestrictionTransferManagerFactory is ModuleFactory {

/**
* @notice Constructor
* @param _polyAddress Address of the polytoken
* @param _setupCost Setup cost of the module
* @param _usageCost Usage cost of the module
* @param _subscriptionCost Subscription cost of the module
*/
constructor (address _polyAddress, uint256 _setupCost, uint256 _usageCost, uint256 _subscriptionCost) public
ModuleFactory(_polyAddress, _setupCost, _usageCost, _subscriptionCost)
{
version = "1.0.0";
name = "VolumeRestrictionTransferManager";
title = "Volume Restriction Transfer Manager";
description = "Manage transfers using lock ups over time";

}

/**
* @notice used to launch the Module with the help of factory
* @return address Contract address of the Module
*/
function deploy(bytes /* _data */) external returns(address) {
if (setupCost > 0)
require(polyToken.transferFrom(msg.sender, owner, setupCost), "Failed transferFrom because of sufficent Allowance is not provided");
address volumeRestrictionTransferManager = new VolumeRestrictionTransferManager(msg.sender, address(polyToken));
emit GenerateModuleFromFactory(address(volumeRestrictionTransferManager), getName(), address(this), msg.sender, now);
return address(volumeRestrictionTransferManager);
}

/**
* @notice Type of the Module factory
*/
function getType() public view returns(uint8) {
return 2;
}

/**
* @notice Get the name of the Module
*/
function getName() public view returns(bytes32) {
return name;
}

/**
* @notice Get the description of the Module
*/
function getDescription() public view returns(string) {
return description;
}

/**
* @notice Get the title of the Module
*/
function getTitle() public view returns(string) {
return title;
}

/**
* @notice Get the version of the Module
*/
function getVersion() external view returns(string) {
return version;
}

/**
* @notice Get the setup cost of the module
*/
function getSetupCost() external view returns (uint256) {
return setupCost;
}

/**
* @notice Get the Instructions that helped to used the module
*/
function getInstructions() public view returns(string) {
return "Allows an issuer to set lockup periods for user addresses, with funds distributed over time. Init function takes no parameters.";
}

/**
* @notice Get the tags related to the module factory
*/
function getTags() public view returns(bytes32[]) {
bytes32[] memory availableTags = new bytes32[](2);
availableTags[0] = "Volume";
availableTags[1] = "Transfer Restriction";
return availableTags;
}


}
15 changes: 15 additions & 0 deletions migrations/2_deploy_contracts.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const GeneralPermissionManagerFactory = artifacts.require('./GeneralPermissionMa
const PercentageTransferManagerFactory = artifacts.require('./PercentageTransferManagerFactory.sol')
const USDTieredSTOProxyFactory = artifacts.require('./USDTieredSTOProxyFactory.sol');
const CountTransferManagerFactory = artifacts.require('./CountTransferManagerFactory.sol')
const VolumeRestrictionTransferManagerFactory = artifacts.require('./VolumeRestrictionTransferManagerFactory.sol')
const EtherDividendCheckpointFactory = artifacts.require('./EtherDividendCheckpointFactory.sol')
const ERC20DividendCheckpointFactory = artifacts.require('./ERC20DividendCheckpointFactory.sol')
const ModuleRegistry = artifacts.require('./ModuleRegistry.sol');
Expand Down Expand Up @@ -148,6 +149,10 @@ const functionSignatureProxyMR = {
// D) Deploy the CountTransferManagerFactory Contract (Factory used to generate the CountTransferManager contract use
// to track the counts of the investors of the security token)
return deployer.deploy(CountTransferManagerFactory, PolyToken, 0, 0, 0, {from: PolymathAccount});
}).then(() => {
// D) Deploy the VolumeRestrictionTransferManagerFactory Contract (Factory used to generate the VolumeRestrictionTransferManager contract use
// to track the counts of the investors of the security token)
return deployer.deploy(VolumeRestrictionTransferManagerFactory, PolyToken, 0, 0, 0, {from: PolymathAccount});
}).then(() => {
// D) Deploy the PercentageTransferManagerFactory Contract (Factory used to generate the PercentageTransferManager contract use
// to track the percentage of investment the investors could do for a particular security token)
Expand All @@ -172,6 +177,10 @@ const functionSignatureProxyMR = {
// D) Register the CountTransferManagerFactory in the ModuleRegistry to make the factory available at the protocol level.
// So any securityToken can use that factory to generate the CountTransferManager contract.
return moduleRegistry.registerModule(CountTransferManagerFactory.address, {from: PolymathAccount});
}).then(() => {
// D) Register the VolumeRestrictionTransferManagerFactory in the ModuleRegistry to make the factory available at the protocol level.
// So any securityToken can use that factory to generate the VolumeRestrictionTransferManager contract.
return moduleRegistry.registerModule(VolumeRestrictionTransferManagerFactory.address, {from: PolymathAccount});
}).then(() => {
// D) Register the GeneralTransferManagerFactory in the ModuleRegistry to make the factory available at the protocol level.
// So any securityToken can use that factory to generate the GeneralTransferManager contract.
Expand Down Expand Up @@ -202,6 +211,11 @@ const functionSignatureProxyMR = {
// contract, Factory should comes under the verified list of factories or those factories deployed by the securityToken issuers only.
// Here it gets verified because it is deployed by the third party account (Polymath Account) not with the issuer accounts.
return moduleRegistry.verifyModule(CountTransferManagerFactory.address, true, {from: PolymathAccount});
}).then(() => {
// G) Once the VolumeRestrictionTransferManagerFactory registered with the ModuleRegistry contract then for making them accessble to the securityToken
// contract, Factory should comes under the verified list of factories or those factories deployed by the securityToken issuers only.
// Here it gets verified because it is deployed by the third party account (Polymath Account) not with the issuer accounts.
return moduleRegistry.verifyModule(VolumeRestrictionTransferManagerFactory.address, true, {from: PolymathAccount});
}).then(() => {
// G) Once the PercentageTransferManagerFactory registered with the ModuleRegistry contract then for making them accessble to the securityToken
// contract, Factory should comes under the verified list of factories or those factories deployed by the securityToken issuers only.
Expand Down Expand Up @@ -304,6 +318,7 @@ const functionSignatureProxyMR = {
USDTieredSTOProxyFactory: ${USDTieredSTOProxyFactory.address}

CountTransferManagerFactory: ${CountTransferManagerFactory.address}
VolumeRestrictionTransferManagerFactory: ${VolumeRestrictionTransferManagerFactory.address}
PercentageTransferManagerFactory: ${PercentageTransferManagerFactory.address}
ManualApprovalTransferManagerFactory:
${ManualApprovalTransferManagerFactory.address}
Expand Down
Loading