diff --git a/CHANGELOG.md b/CHANGELOG.md index e9f5ed668..b767eef56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. [__1.5.0__](https://www.npmjs.com/package/polymath-core?activeTab=readme) __15-08-18__ ## Added +* Added `getTokensByOwner` to STR * Added withholding tax to ether & erc20 dividends * Generalised MakerDAO oracle to allow different instances referencing different currencies * Added DAI as a fundraising currency to USDTieredSTO diff --git a/contracts/SecurityTokenRegistry.sol b/contracts/SecurityTokenRegistry.sol index 36af1baf4..189328f09 100644 --- a/contracts/SecurityTokenRegistry.sol +++ b/contracts/SecurityTokenRegistry.sol @@ -1,7 +1,7 @@ pragma solidity ^0.4.24; import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "./interfaces/IOwner.sol"; +import "./interfaces/IOwnable.sol"; import "./interfaces/ISTFactory.sol"; import "./interfaces/IERC20.sol"; import "./interfaces/ISecurityTokenRegistry.sol"; @@ -25,6 +25,9 @@ contract SecurityTokenRegistry is ISecurityTokenRegistry, EternalStorage { address public owner; address public polymathRegistry; + address[] public activeUsers; + mapping(address => bool) public seenUsers; + mapping(address => bytes32[]) userToTickers; mapping(string => address) tickerToSecurityToken; mapping(string => uint) tickerIndex; @@ -143,6 +146,11 @@ contract SecurityTokenRegistry is ISecurityTokenRegistry, EternalStorage { require(IERC20(getAddress(Encoder.getKey("polyToken"))).transferFrom(msg.sender, address(this), getUint(Encoder.getKey("tickerRegFee"))), "Sufficent allowance is not provided"); string memory ticker = Util.upper(_ticker); require(_tickerAvailable(ticker), "Ticker is already reserved"); + // Check whether ticker was previously registered (and expired) + address previousOwner = getAddress(Encoder.getKey("registeredTickers_owner", _ticker)); + if (previousOwner != address(0)) { + _deleteTickerOwnership(previousOwner, _ticker); + } _addTicker(_owner, ticker, _tokenName, now, now.add(getUint(Encoder.getKey("expiryLimit"))), false, false); } @@ -232,6 +240,10 @@ contract SecurityTokenRegistry is ISecurityTokenRegistry, EternalStorage { uint256 length = uint256(getArrayBytes32(Encoder.getKey("userToTickers", _owner)).length); pushArray(Encoder.getKey("userToTickers", _owner), Util.stringToBytes32(_ticker)); set(Encoder.getKey("tickerIndex", _ticker), length); + if (!getBool(Encoder.getKey("seenUsers", _owner))) { + pushArray(Encoder.getKey("activeUsers"), _owner); + set(Encoder.getKey("seenUsers", _owner), true); + } } /** @@ -280,6 +292,7 @@ contract SecurityTokenRegistry is ISecurityTokenRegistry, EternalStorage { */ function _deleteTickerOwnership(address _owner, string _ticker) internal { uint256 _index = uint256(getUint(Encoder.getKey("tickerIndex", _ticker))); + assert(_index < getArrayBytes32(Encoder.getKey("userToTickers", _owner)).length); // deleting the _index from the data strucutre userToTickers[_oldowner][_index]; deleteArrayBytes32(Encoder.getKey("userToTickers", _owner), _index); @@ -318,6 +331,48 @@ contract SecurityTokenRegistry is ISecurityTokenRegistry, EternalStorage { return tempList; } + /** + * @notice Returns the list of tokens owned by the selected address + * @param _owner is the address which owns the list of tickers + * @dev Intention is that this is called off-chain so block gas limit is not relevant + */ + function getTokensByOwner(address _owner) external view returns(address[]) { + // Loop over all active users, then all associated tickers of those users + // This ensures we find tokens, even if their owner has been modified + address[] memory activeUsers = getArrayAddress(Encoder.getKey("activeUsers")); + bytes32[] memory tickers; + address token; + uint256 count = 0; + uint256 i = 0; + uint256 j = 0; + for (i = 0; i < activeUsers.length; i++) { + tickers = getArrayBytes32(Encoder.getKey("userToTickers", activeUsers[i])); + for (j = 0; j < tickers.length; j++) { + token = getAddress(Encoder.getKey("tickerToSecurityToken", Util.bytes32ToString(tickers[j]))); + if (token != address(0)) { + if (IOwnable(token).owner() == _owner) { + count = count + 1; + } + } + } + } + uint256 index = 0; + address[] memory result = new address[](count); + for (i = 0; i < activeUsers.length; i++) { + tickers = getArrayBytes32(Encoder.getKey("userToTickers", activeUsers[i])); + for (j = 0; j < tickers.length; j++) { + token = getAddress(Encoder.getKey("tickerToSecurityToken", Util.bytes32ToString(tickers[j]))); + if (token != address(0)) { + if (IOwnable(token).owner() == _owner) { + result[index] = token; + index = index + 1; + } + } + } + } + return result; + } + /** * @notice Returns the owner and timestamp for a given ticker * @param _ticker is the ticker symbol @@ -442,7 +497,7 @@ contract SecurityTokenRegistry is ISecurityTokenRegistry, EternalStorage { function getSecurityTokenData(address _securityToken) external view returns (string, address, string, uint256) { return ( getString(Encoder.getKey("securityTokens_ticker", _securityToken)), - IOwner(_securityToken).owner(), + IOwnable(_securityToken).owner(), getString(Encoder.getKey("securityTokens_tokenDetails", _securityToken)), getUint(Encoder.getKey("securityTokens_deployedAt", _securityToken)) ); diff --git a/contracts/interfaces/IOwnable.sol b/contracts/interfaces/IOwnable.sol new file mode 100644 index 000000000..15849e181 --- /dev/null +++ b/contracts/interfaces/IOwnable.sol @@ -0,0 +1,26 @@ +pragma solidity ^0.4.24; + + +/** + * @title Ownable + * @dev The Ownable contract has an owner address, and provides basic authorization control + * functions, this simplifies the implementation of "user permissions". + */ +interface IOwnable { + /** + * @dev Returns owner + */ + function owner() external returns (address); + + /** + * @dev Allows the current owner to relinquish control of the contract. + */ + function renounceOwnership() external; + + /** + * @dev Allows the current owner to transfer control of the contract to a newOwner. + * @param _newOwner The address to transfer ownership to. + */ + function transferOwnership(address _newOwner) external; + +} diff --git a/contracts/interfaces/IOwner.sol b/contracts/interfaces/IOwner.sol deleted file mode 100644 index f76b3516b..000000000 --- a/contracts/interfaces/IOwner.sol +++ /dev/null @@ -1,5 +0,0 @@ -pragma solidity ^0.4.24; - -contract IOwner { - address public owner; -} \ No newline at end of file diff --git a/contracts/interfaces/ISecurityTokenRegistry.sol b/contracts/interfaces/ISecurityTokenRegistry.sol index 4a30ce99c..3d5cc0736 100644 --- a/contracts/interfaces/ISecurityTokenRegistry.sol +++ b/contracts/interfaces/ISecurityTokenRegistry.sol @@ -55,7 +55,7 @@ interface ISecurityTokenRegistry { * @dev Allows the current owner to transfer control of the contract to a newOwner. * @param _newOwner The address to transfer ownership to. */ - function transferOwnership(address _newOwner) external; + function transferOwnership(address _newOwner) external; /** * @notice Get security token address by ticker name @@ -85,6 +85,13 @@ interface ISecurityTokenRegistry { */ function getTickersByOwner(address _owner) external view returns(bytes32[]); + /** + * @notice Returns the list of tokens owned by the selected address + * @param _owner is the address which owns the list of tickers + * @dev Intention is that this is called off-chain so block gas limit is not relevant + */ + function getTokensByOwner(address _owner) external view returns(address[]); + /** * @notice Returns the owner and timestamp for a given ticker * @param _ticker ticker diff --git a/test/n_security_token_registry.js b/test/n_security_token_registry.js index 38e28315e..ceef4c9f3 100644 --- a/test/n_security_token_registry.js +++ b/test/n_security_token_registry.js @@ -618,6 +618,9 @@ contract('SecurityTokenRegistry', accounts => { assert.equal(tx.logs[1].args._ticker, symbol2, "SecurityToken doesn't get deployed"); I_SecurityToken002 = SecurityToken.at(tx.logs[1].args._securityTokenAddress); + let tokens = await I_STRProxied.getTokensByOwner.call(token_owner); + assert.equal(tokens[0], I_SecurityToken.address); + assert.equal(tokens[1], I_SecurityToken002.address); const log = await promisifyLogWatch(I_SecurityToken002.LogModuleAdded({from: _blockNo}), 1); // Verify that GeneralTransferManager module get added successfully or not @@ -1084,7 +1087,7 @@ contract('SecurityTokenRegistry', accounts => { let tickersList = await I_STRProxied.getTickersByOwner.call(token_owner); assert.equal(tickersList.length, 4); let tickersListArray = await I_STRProxied.getTickersByOwner.call(account_temp); - assert.equal(tickersListArray.length, 3); + assert.equal(tickersListArray.length, 2); }); }); diff --git a/test/o_security_token.js b/test/o_security_token.js index ce24a9be0..f94354bae 100644 --- a/test/o_security_token.js +++ b/test/o_security_token.js @@ -282,7 +282,6 @@ contract('SecurityToken', accounts => { await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); let _blockNo = latestBlock(); let tx = await I_STRProxied.generateSecurityToken(name, symbol, tokenDetails, false, { from: token_owner, gas:60000000 }); - // Verify the successful generation of the security token assert.equal(tx.logs[1].args._ticker, symbol, "SecurityToken doesn't get deployed");