Skip to content

Commit

Permalink
Merge pull request #39 from PolymathNetwork/extendable_modules
Browse files Browse the repository at this point in the history
Functionality to allow Exchanges to manage their own whitelists
  • Loading branch information
adamdossa authored Mar 29, 2018
2 parents 49f34f3 + 52c1d48 commit b82a1a9
Show file tree
Hide file tree
Showing 8 changed files with 606 additions and 19 deletions.
11 changes: 11 additions & 0 deletions contracts/ModuleRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity ^0.4.18;
import './interfaces/IModuleRegistry.sol';
import './interfaces/IModuleFactory.sol';
import './interfaces/ISecurityToken.sol';
import './interfaces/ISecurityTokenRegistry.sol';
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';

/**
Expand All @@ -18,19 +19,29 @@ contract ModuleRegistry is IModuleRegistry, Ownable {
mapping (uint8 => address[]) public moduleList;
mapping (address => bool) public verified;

address public securityTokenRegistry;

event LogModuleUsed(address indexed _moduleFactory, address indexed _securityToken);
event LogModuleRegistered(address indexed _moduleFactory, address indexed _owner);
event LogModuleVerified(address indexed _moduleFactory, bool _verified);

//Checks that module is correctly configured in registry
function useModule(address _moduleFactory) external {
//msg.sender must be a security token - below will throw if not
ISecurityTokenRegistry(securityTokenRegistry).getSecurityTokenData(msg.sender);
require(registry[_moduleFactory] != 0);
//To use a module, either it must be verified, or owned by the ST owner
require(verified[_moduleFactory] || (IModuleFactory(_moduleFactory).owner() == ISecurityToken(msg.sender).owner()));
reputation[_moduleFactory].push(msg.sender);
LogModuleUsed(_moduleFactory, msg.sender);
}

//Sets the securityTokenRegistry so that moduleRegistry can validate security tokens are genuine
function setTokenRegistry(address _securityTokenRegistry) public onlyOwner {
require(_securityTokenRegistry != address(0));
securityTokenRegistry = _securityTokenRegistry;
}

/**
* @dev Called by Polymath to register new modules for SecurityToken to use
* @param _moduleFactory is the address of the module factory to be registered
Expand Down
6 changes: 6 additions & 0 deletions contracts/interfaces/IModule.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pragma solidity ^0.4.18;

import './ISecurityToken.sol';
import './IModuleFactory.sol';

//Simple interface that any module contracts should implement
contract IModule {
Expand Down Expand Up @@ -34,5 +35,10 @@ contract IModule {
_;
}

modifier onlyFactoryOwner {
require(msg.sender == IModuleFactory(factory).owner());
_;
}

function permissions() public returns(bytes32[]);
}
60 changes: 60 additions & 0 deletions contracts/modules/TransferManager/ExchangeTransferManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
pragma solidity ^0.4.18;

import './ITransferManager.sol';

/////////////////////
// Module permissions
/////////////////////
// Factory Owner
// modifyWhitelist X
// modifyWhitelistMulti X

contract ExchangeTransferManager is ITransferManager {

address public exchange;
mapping (address => bool) public whitelist;

event LogModifyWhitelist(address _investor, uint256 _dateAdded, address _addedBy);

function ExchangeTransferManager(address _securityToken)
IModule(_securityToken)
public
{
}

function configure(address _exchange) public onlyFactory {
exchange = _exchange;
}

function getInitFunction() public returns(bytes4) {
return bytes4(keccak256("configure(address)"));
}

function verifyTransfer(address _from, address _to, uint256 _amount) view external returns(bool) {
//Transfer must be from / to the exchange
require((_from == exchange) || (_to == exchange));
return getExchangePermission(_from) || getExchangePermission(_to);
}

function getExchangePermission(address _investor) view internal returns (bool) {
//This function could implement more complex logic, e.g. calling out to another contract maintained by the exchange to get list of allowed users
return whitelist[_investor];
}

function modifyWhitelist(address _investor, bool _valid) public onlyFactoryOwner {
whitelist[_investor] = _valid;
LogModifyWhitelist(_investor, now, msg.sender);
}

function modifyWhitelistMulti(address[] _investors, bool[] _valids) public onlyFactoryOwner {
require(_investors.length == _valids.length);
for (uint256 i = 0; i < _investors.length; i++) {
modifyWhitelist(_investors[i], _valids[i]);
}
}

function permissions() public returns(bytes32[]) {
bytes32[] memory allPermissions = new bytes32[](0);
return allPermissions;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
pragma solidity ^0.4.18;

import './ExchangeTransferManager.sol';
import '../../interfaces/IModuleFactory.sol';

contract ExchangeTransferManagerFactory is IModuleFactory {

function deploy(bytes _data) external returns(address) {
//polyToken.transferFrom(msg.sender, owner, getCost());
ExchangeTransferManager exchangeTransferManager = new ExchangeTransferManager(msg.sender);
require(getSig(_data) == exchangeTransferManager.getInitFunction());
require(address(exchangeTransferManager).call(_data));
return address(exchangeTransferManager);

}

function getCost() view external returns(uint256) {
return 0;
}

function getType() view external returns(uint8) {
return 2;
}

function getName() view external returns(bytes32) {
return "ExchangeTransferManager";
}

function getDescription() view external returns(string) {
return "Manage transfers within an exchange";
}

function getTitle() view external returns(string) {
return "Exchange Transfer Manager";
}

}
68 changes: 50 additions & 18 deletions contracts/tokens/SecurityToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,27 @@ contract SecurityToken is ISecurityToken, StandardToken, DetailedERC20 {
// TransferManager has a key of 2
// STO has a key of 3
// Other modules TBD
mapping (uint8 => ModuleData) public modules;
// Module list should be order agnostic!
mapping (uint8 => ModuleData[]) public modules;

uint8 public MAX_MODULES = 10;

event LogModuleAdded(uint8 indexed _type, bytes32 _name, address _moduleFactory, address _module, uint256 _moduleCost, uint256 _budget, uint256 _timestamp);
event LogModuleRemoved(uint8 indexed _type, address _module, uint256 _timestamp);
event LogModuleBudgetChanged(uint8 indexed _moduleType, address _module, uint256 _budget);
event Mint(address indexed to, uint256 amount);

//if _fallback is true, then we only allow the module if it is set, if it is not set we only allow the owner
modifier onlyModule(uint8 _i, bool _fallback) {
if (_fallback && (address(0) == modules[_i].moduleAddress)) {
modifier onlyModule(uint8 _moduleType, bool _fallback) {
//Loop over all modules of type _moduleType
bool isModuleType = false;
for (uint8 i = 0; i < modules[_moduleType].length; i++) {
isModuleType = isModuleType || (modules[_moduleType][i].moduleAddress == msg.sender);
}
if (_fallback && !isModuleType) {
require(msg.sender == owner);
} else {
require(msg.sender == modules[_i].moduleAddress);
require(isModuleType);
}
_;
}
Expand All @@ -71,8 +80,7 @@ contract SecurityToken is ISecurityToken, StandardToken, DetailedERC20 {
//owner = _owner;
}

function addModule(address _moduleFactory, bytes _data, uint256 _maxCost, uint256 _budget, bool _replaceable) external {
require(msg.sender == owner);
function addModule(address _moduleFactory, bytes _data, uint256 _maxCost, uint256 _budget, bool _replaceable) external onlyOwner {
_addModule(_moduleFactory, _data, _maxCost, _budget, _replaceable);
}

Expand All @@ -85,16 +93,18 @@ contract SecurityToken is ISecurityToken, StandardToken, DetailedERC20 {
* @param _maxCost max amount of POLY willing to pay to module. (WIP)
* @param _replaceable whether or not the module is supposed to be replaceable
*/
//You are only ever allowed one instance, for a given module type
//You are allowed to add a new moduleType if:
// - there is no existing module of that type yet added
// - the last member of the module list is replacable
function _addModule(address _moduleFactory, bytes _data, uint256 _maxCost, uint256 _budget, bool _replaceable) internal {
//Check that module exists in registry - will throw otherwise
IModuleRegistry(moduleRegistry).useModule(_moduleFactory);
IModuleFactory moduleFactory = IModuleFactory(_moduleFactory);
uint256 moduleCost = moduleFactory.getCost();
require(moduleCost <= _maxCost);
//Check that this module has not already been set as non-replaceable
if (modules[moduleFactory.getType()].moduleAddress != address(0)) {
require(modules[moduleFactory.getType()].replaceable);
if (modules[moduleFactory.getType()].length != 0) {
require(modules[moduleFactory.getType()][modules[moduleFactory.getType()].length - 1].replaceable);
}
//Approve fee for module
require(polyToken.approve(_moduleFactory, moduleCost));
Expand All @@ -103,20 +113,30 @@ contract SecurityToken is ISecurityToken, StandardToken, DetailedERC20 {
//Approve ongoing budget
require(polyToken.approve(module, _budget));
//Add to SecurityToken module map
modules[moduleFactory.getType()] = ModuleData(moduleFactory.getName(), module, _replaceable);
modules[moduleFactory.getType()].push(ModuleData(moduleFactory.getName(), module, _replaceable));
//Emit log event
LogModuleAdded(moduleFactory.getType(), moduleFactory.getName(), _moduleFactory, module, moduleCost, _budget, now);
}

function removeModule(uint8 _moduleType, uint8 _moduleIndex) external onlyOwner {
require(_moduleIndex < modules[_moduleType].length);
require(modules[_moduleType][_moduleIndex].moduleAddress != address(0));
require(modules[_moduleType][_moduleIndex].replaceable);
//Take the last member of the list, and replace _moduleIndex with this, then shorten the list by one
LogModuleRemoved(_moduleType, modules[_moduleType][_moduleIndex].moduleAddress, now);
modules[_moduleType][_moduleIndex] = modules[_moduleType][modules[_moduleType].length - 1];
modules[_moduleType].length = modules[_moduleType].length - 1;
}

function withdrawPoly(uint256 _amount) public onlyOwner {
require(polyToken.transfer(owner, _amount));
}

function changeModuleBudget(uint8 _moduleType, uint256 _budget) public onlyOwner {
function changeModuleBudget(uint8 _moduleType, uint8 _moduleIndex, uint256 _budget) public onlyOwner {
require(_moduleType != 0);
require(modules[_moduleType].moduleAddress != address(0));
require(polyToken.approve(modules[_moduleType].moduleAddress, _budget));
LogModuleBudgetChanged(_moduleType, modules[_moduleType].moduleAddress, _budget);
require(_moduleIndex < modules[_moduleType].length);
require(polyToken.approve(modules[_moduleType][_moduleType].moduleAddress, _budget));
LogModuleBudgetChanged(_moduleType, modules[_moduleType][_moduleType].moduleAddress, _budget);
}

/**
Expand All @@ -138,10 +158,15 @@ contract SecurityToken is ISecurityToken, StandardToken, DetailedERC20 {
// Permissions this to a TransferManager module, which has a key of 2
// If no TransferManager return true
function verifyTransfer(address _from, address _to, uint256 _amount) view public returns (bool success) {
if (modules[2].moduleAddress == address(0)) {
if (modules[2].length == 0) {
return true;
}
return ITransferManager(modules[2].moduleAddress).verifyTransfer(_from, _to, _amount);
for (uint8 i = 0; i < modules[2].length; i++) {
if (ITransferManager(modules[2][i].moduleAddress).verifyTransfer(_from, _to, _amount)) {
return true;
}
}
return false;
}

// Only STO module can call this, has a key of 3
Expand All @@ -163,10 +188,17 @@ contract SecurityToken is ISecurityToken, StandardToken, DetailedERC20 {
// If no Permission return false - note that IModule withPerm will allow ST owner all permissions anyway
// this allows individual modules to override this logic if needed (to not allow ST owner all permissions)
function checkPermission(address _delegate, address _module, bytes32 _perm) view public returns(bool) {
if (modules[1].moduleAddress == address(0)) {

if (modules[1].length == 0) {
return false;
}
return IPermissionManager(modules[1].moduleAddress).checkPermission(_delegate, _module, _perm);

for (uint8 i = 0; i < modules[1].length; i++) {
if (IPermissionManager(modules[1][i].moduleAddress).checkPermission(_delegate, _module, _perm)) {
return true;
}
}

}

}
53 changes: 53 additions & 0 deletions docs/multiple_transfer_managers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Multiple Transfer Managers

We consider the two most common types of exchanges:
- a CEX where the Issuer is willing to cede some control to the CEX for authorising users
- a DEX where the Issuer wants to maintain full control over who can trade tokens

We also consider the case where an Issuer may want to have multiple KYC providers

## CEX

The Issuer and the CEX reach an agreement that the CEX is allowed to authorise users to transfer and receive tokens from the CEX, following an appropriate authorisation process for the user.

The Issuer and the CEX should agree the set of rules which an investor must meet in order to become authorised.

If these rules depend on the current `GeneralTransferManager` state (e.g. whether a user is already on the `GeneralTransferManager` or not, and / or their time based restrictions) then the `ExchangeTransferManager` may need to be passed the `GeneralTransferManager` in its `initFunction` so that it can appropriately enforce these rules when adding users to the `ExchangeTransferManager` whitelist by referencing the state of the `GeneralTransferManager`.

The Issuer adds the CEX's `ExchangeTransferManager` to its token as a `TransferManager` module.

The logic for verifying transfers in the `ExchangeTransferManager` will likely depend on the specific exchange, but as an example we consider the following agreement / rules:
- the CEX is allowed to authorise transfers between itself and users (representing deposits / withdrawals)
- in other words, the CEX TransferManager will only verify transfers between the CEX wallet address and its authorised users.

As users are authorised by the CEX, the CEX adds the users address to the whitelist on the `ExchangeTransferManager` instance.

If the user wants to deposit funds to the exchange, then a `ST.transfer(userAddress, exchangeAddress, amount)` is triggered. Since the user has been added to the `ExchangeTransferManager`, this transfer will be deemed valid by the `SecurityToken`.

If the user wants to withdraw funds from the exchange, then a `ST.transfer(exchangeAddress, userAddress, amount)` is triggered. Since the user has been added to the `ExchangeTransferManager`, this transfer will be deemed valid by the `SecurityToken`.

The CEX should ensure that before allowing a user to purchase a particular token on its exchange, that the user is authorised to do so, and has been added to the corresponding `SecurityToken`'s `ExchangeTransferManager`. This avoids the problem where a user may buy a token on an exchange, but then not be able to subsequently withdraw it. i.e. it should call `verifyTransfer(exchangeAddress, userAddress, amount)` and only allow the trade if this returns `true`.

### Notes

It is possible for the CEX to maintain it's own contract which lists all of its authorised users across all security tokens. Its `ExchangeTransferManager` can then reference this global list, possibly enforcing additional criteria on users metadata (e.g. their jurisdiction and so on). This would allow it to easily offer `ExchangeTransferManager`'s for its exchange to multiple Issuers at a very low additional cost to itself (since the `ExchangeTransferManager` is generic and just references the exchanges own contract where it maintains a global list of authorised users). Obviously the Issuer would need to agree to this approach.

It may be that the exchange doesn't have a single exchange address (i.e. each user has their own wallet) which would make the implementation harder. This logic would then need to be determined on a case by case basis, but the exchange should be able to write their own TransferManager with appropriate rules and add it to the Polymath ecosystem.

## DEX

It may be that the Issuer does not want to delegate any responsibility to the DEX to authorise users.

In this case, the following is a straightforward approach:
- DEX contract is added to the `GeneralTransferManager` by the Issuer (probably with no time restrictions).
- Before allowing a user to deposit to the DEX, the DEX calls `verifyTransfer(DEXAddress, userAddress, amount)` and only allows the trade if this returns `true`. This ensures that the user would be able to withdraw any deposited tokens if they decide to.
- Before allowing a user to buy tokens on the DEX, the DEX calls `verifyTransfer(DEXAddress, userAddress, amount)` and only allows the trade if this returns `true`. This ensures that the user would be able to withdraw any deposited tokens if they decide to.
- The check to `verifyTransfer(userAddress, DEXAddress, amount)` is implicit in the deposit transfer, but an exchange could also call this explicitly if they wanted to, and for example only show SecurityTokens to the user for which this returns `true`.

With this approach, users can only transfer tokens to the exchange if they are on the `GeneralTransferManager` with a `fromTime` in the past, and users can only purchase tokens on the exchange if they are on the `GeneralTransferManager` with a `toTime` in the past.

## Multiple KYC Providers

An issuer may want to have multiple KYC providers. In this case it is straightforward to add multiple TransferManagers to their SecurityToken, one per KYC provider, and each KYC provider can operate independently (with the full set of authorised users being users who are authorised by any single KYC provider).

The Issuer could subsequently remove one of the KYC providers TransferManagers from their SecurityToken if they were unhappy with the service (although they would not get any POLY paid for the service refunded).
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit b82a1a9

Please sign in to comment.