diff --git a/.eslintrc.js b/.eslintrc.js index 8b0c0ac0f..6442660d9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,6 +15,9 @@ module.exports = { "semi": 0, "no-undef": 0, "key-spacing": 0, + "node":0, + "spaced-comment":0, + "node/no-deprecated-api":0 "no-tabs": 0, "no-mixed-spaces-and-tabs":0 } diff --git a/contracts/modules/Experimental/TransferManager/SignedTransferManager.sol b/contracts/modules/Experimental/TransferManager/SignedTransferManager.sol new file mode 100644 index 000000000..12699f74f --- /dev/null +++ b/contracts/modules/Experimental/TransferManager/SignedTransferManager.sol @@ -0,0 +1,164 @@ +pragma solidity ^0.4.24; + +import "../../TransferManager/ITransferManager.sol"; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; + +/** + * @title Transfer Manager module for verifing transations with a signed message + */ +contract SignedTransferManager is ITransferManager { + using SafeMath for uint256; + + bytes32 constant public ADMIN = "ADMIN"; + + //Keeps track of if the signature has been used or invalidated + mapping(bytes => bool) invalidSignatures; + + //keep tracks of the address that allows to sign messages + mapping(address => bool) public signers; + + + // Emit when signer stats was changed + event UpdateSigners(address[] _signers, bool[] _signersStats); + + // Emit when a signature has been deemed invalid + event InvalidSignature(bytes _data); + + + /** + * @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 This function returns the signature of configure function + */ + function getInitFunction() public pure returns (bytes4) { + return bytes4(0); + } + + /** + * @notice function to check if a signature is still valid + * @param _data signature + */ + function checkSignatureIsInvalid(bytes _data) public view returns(bool){ + return invalidSignatures[_data]; + } + + /** + * @notice function to remove or add signer(s) onto the signer mapping + * @param _signers address array of signers + * @param _signersStats bool array of signers stats + */ + function updateSigners(address[] _signers, bool[] _signersStats) public withPerm(ADMIN) { + require(_signers.length == _signersStats.length, "input array length does not match"); + for(uint256 i=0; i<_signers.length; i++){ + signers[_signers[i]] = _signersStats[i]; + } + emit UpdateSigners(_signers, _signersStats); + } + + /** + * @notice allow verify transfer with signature + * @param _from address transfer from + * @param _to address transfer to + * @param _amount transfer amount + * @param _data signature + * @param _isTransfer bool value of isTransfer + * Sig needs to be valid (not used or deemed as invalid) + * Signer needs to be in the signers mapping + */ + function verifyTransfer(address _from, address _to, uint256 _amount, bytes _data , bool _isTransfer) public returns(Result) { + if (!paused) { + + require (_isTransfer == false || msg.sender == securityToken, "Sender is not the owner"); + + // not using require to avoid revert in this function + + if(_data.length == 0){ + return Result.INVALID; // data input check + } + + require(invalidSignatures[_data] != true, "Invalid signature - signature is either used or deemed as invalid"); + bytes32 hash = keccak256(abi.encodePacked(this, _from, _to, _amount)); + bytes32 prependedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + address signer = _recoverSignerAdd(prependedHash, _data); + + if (signers[signer] != true){ + return Result.NA; + } else if(_isTransfer == true) { + invalidSignatures[_data] = true; + return Result.VALID; + } else { + return Result.VALID; + } + } + return Result.NA; + } + + /** + * @notice allow signers to deem a signature invalid + * @param _from address transfer from + * @param _to address transfer to + * @param _amount transfer amount + * @param _data signature + * Sig needs to be valid (not used or deemed as invalid) + * Signer needs to be in the signers mapping + */ + function invalidSignature(address _from, address _to, uint256 _amount, bytes _data) public { + require(signers[msg.sender] == true, "Only signer is allowed to invalid signature."); + require(invalidSignatures[_data] != true, "This signature is invalid."); + + bytes32 hash = keccak256(abi.encodePacked(this, _from, _to, _amount)); + bytes32 prependedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + + // return signer; + require(_recoverSignerAdd(prependedHash,_data) == msg.sender, "Incorrect Signer for this signature"); + + invalidSignatures[_data] = true; + emit InvalidSignature(_data); + } + + /** + * @notice used to recover signers' add from signature + */ + function _recoverSignerAdd(bytes32 _hash, bytes _data) internal pure returns(address) { + + //Check that the signature is valid + require(_data.length == 65, "Date input length is invalid"); + + bytes32 r; + bytes32 s; + uint8 v; + + assembly { + r := mload(add(_data, 32)) + s := mload(add(_data, 64)) + v := and(mload(add(_data, 65)), 255) + } + if (v < 27) { + v += 27; + } + if (v != 27 && v != 28) { + return 0; + } + + return ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _hash)), v, r, s); + } + + + /** + * @notice Return the permissions flag that are associated with ManualApproval transfer manager + */ + function getPermissions() public view returns(bytes32[]) { + bytes32[] memory allPermissions = new bytes32[](1); + allPermissions[0] = ADMIN; + return allPermissions; + } +} diff --git a/contracts/modules/Experimental/TransferManager/SignedTransferManagerFactory.sol b/contracts/modules/Experimental/TransferManager/SignedTransferManagerFactory.sol new file mode 100644 index 000000000..af26dfcb2 --- /dev/null +++ b/contracts/modules/Experimental/TransferManager/SignedTransferManagerFactory.sol @@ -0,0 +1,102 @@ +pragma solidity ^0.4.24; + +import "./SignedTransferManager.sol"; +import "../../ModuleFactory.sol"; + +/** + * @title Factory for deploying SignedTransferManager module + */ +contract SignedTransferManagerFactory is ModuleFactory { + + /** + * @notice Constructor + * @param _polyAddress Address of the polytoken + */ + constructor (address _polyAddress, uint256 _setupCost, uint256 _usageCost, uint256 _subscriptionCost) public + ModuleFactory(_polyAddress, _setupCost, _usageCost, _subscriptionCost) + { + version = "1.0.0"; + name = "SignedTransferManager"; + title = "Signed Transfer Manager"; + description = "Manage transfers using a signature"; + compatibleSTVersionRange["lowerBound"] = VersionUtils.pack(uint8(0), uint8(0), uint8(0)); + compatibleSTVersionRange["upperBound"] = VersionUtils.pack(uint8(0), uint8(0), uint8(0)); + } + + + /** + * @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 signedTransferManager = new SignedTransferManager(msg.sender, address(polyToken)); + emit GenerateModuleFromFactory(address(signedTransferManager), getName(), address(this), msg.sender, setupCost, now); + return address(signedTransferManager); + } + + + /** + * @notice Type of the Module factory + */ + function getTypes() external view returns(uint8[]) { + uint8[] memory res = new uint8[](1); + res[0] = 2; + return res; + } + + /** + * @notice Get the name of the Module + */ + function getName() public view returns(bytes32) { + return name; + } + + /** + * @notice Get the description of the Module + */ + function getDescription() external view returns(string) { + return description; + } + + /** + * @notice Get the version of the Module + */ + function getVersion() external view returns(string) { + return version; + } + + /** + * @notice Get the title of the Module + */ + function getTitle() external view returns(string) { + return title; + } + + /** + * @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() external view returns(string) { + return "Allows an issuer to maintain a list of signers who can validate transfer request using signatures. A mapping is used to track valid signers which can be managed by the issuer. verifytransfer function takes in a signature and if the signature is valid, it will verify the transfer. invalidSigature function allow the signer to make a signature invalid after it is signed. 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] = "General"; + availableTags[1] = "Transfer Restriction"; + return availableTags; + } + + +} diff --git a/test/helpers/createInstances.js b/test/helpers/createInstances.js index 4335e52b3..36caaac95 100644 --- a/test/helpers/createInstances.js +++ b/test/helpers/createInstances.js @@ -31,6 +31,7 @@ const PreSaleSTOFactory = artifacts.require("./PreSaleSTOFactory.sol"); const PolyToken = artifacts.require("./PolyToken.sol"); const PolyTokenFaucet = artifacts.require("./PolyTokenFaucet.sol"); const DummySTOFactory = artifacts.require("./DummySTOFactory.sol"); +const SignedTransferManagerFactory = artifacts.require("./SignedTransferManagerFactory"); const MockBurnFactory = artifacts.require("./MockBurnFactory.sol"); const MockWrongTypeFactory = artifacts.require("./MockWrongTypeFactory.sol"); const VolumeRestrictionTMFactory = artifacts.require("./VolumeRestrictionTMFactory.sol"); @@ -38,6 +39,7 @@ const VolumeRestrictionTM = artifacts.require("./VolumeRestrictionTM.sol"); const VestingEscrowWalletFactory = artifacts.require("./VestingEscrowWalletFactory.sol"); const VestingEscrowWallet = artifacts.require("./VestingEscrowWallet.sol"); + const Web3 = require("web3"); const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); // Hardcoded development port @@ -80,6 +82,8 @@ let I_BlacklistTransferManagerFactory; let I_VestingEscrowWalletLogic; let I_STRProxied; let I_MRProxied; +let I_SignedTransferManagerFactory; + // Initial fee for ticker registry and security token registry const initRegFee = web3.utils.toWei("250"); @@ -312,6 +316,18 @@ export async function deployLockupVolumeRTMAndVerified(accountPolymath, MRProxyI return new Array(I_VolumeRestrictionTransferManagerFactory); } +export async function deploySignedTMAndVerifyed(accountPolymath, MRProxyInstance, polyToken, setupCost) { + I_SignedTransferManagerFactory = await SignedTransferManagerFactory.new(polyToken, setupCost, 0, 0, { from: accountPolymath }); + assert.notEqual( + I_SignedTransferManagerFactory.address.valueOf(), + "0x0000000000000000000000000000000000000000", + "SignedTransferManagerFactory contract was not deployed" + ); + + await registerAndVerifyByMR(I_SignedTransferManagerFactory.address, accountPolymath, MRProxyInstance); + return new Array(I_SignedTransferManagerFactory); +} + export async function deployScheduleCheckpointAndVerified(accountPolymath, MRProxyInstance, polyToken, setupCost) { I_ScheduledCheckpointFactory = await ScheduledCheckpointFactory.new(polyToken, setupCost, 0, 0, { from: accountPolymath }); assert.notEqual( diff --git a/test/helpers/signData.js b/test/helpers/signData.js index ea7476cc3..549ebdf4f 100644 --- a/test/helpers/signData.js +++ b/test/helpers/signData.js @@ -4,6 +4,7 @@ const ethUtil = require("ethereumjs-util"); //this, _investor, _fromTime, _toTime, _validTo function signData(tmAddress, investorAddress, fromTime, toTime, expiryTime, restricted, validFrom, validTo, nonce, pk) { + let packedData = utils .solidityKeccak256( ["address", "address", "uint256", "uint256", "uint256", "bool", "uint256", "uint256", "uint256"], @@ -16,6 +17,21 @@ function signData(tmAddress, investorAddress, fromTime, toTime, expiryTime, rest return ethUtil.ecsign(new Buffer(packedData.slice(2), "hex"), new Buffer(pk, "hex")); } +// sign data for verify tranfer function +function signDataVerifyTransfer (tmAddress, fromAddress, toAddress, amount, account) { + let packedData = utils + .solidityKeccak256( + ["address", "address", "address", "uint256"], + [tmAddress, fromAddress, toAddress, amount] + ) + .slice(2); + packedData = new Buffer(packedData, "hex"); + packedData = Buffer.concat([new Buffer(`\x19Ethereum Signed Message:\n${packedData.length.toString()}`), packedData]); + packedData = web3.sha3(`0x${packedData.toString("hex")}`, { encoding: "hex" }); + + return web3.eth.sign(account, packedData); +} + module.exports = { - signData + signData, signDataVerifyTransfer }; diff --git a/test/y_signed_transfer_manager.js b/test/y_signed_transfer_manager.js new file mode 100644 index 000000000..1f5ffc122 --- /dev/null +++ b/test/y_signed_transfer_manager.js @@ -0,0 +1,295 @@ +import latestTime from "./helpers/latestTime"; +import { duration, promisifyLogWatch, latestBlock } from "./helpers/utils"; +import takeSnapshot, { increaseTime, revertToSnapshot } from "./helpers/time"; +import { signDataVerifyTransfer } from "./helpers/signData"; +import { pk } from "./helpers/testprivateKey"; +import { encodeProxyCall, encodeModuleCall } from "./helpers/encodeCall"; +import { catchRevert } from "./helpers/exceptions"; +import { setUpPolymathNetwork, deployGPMAndVerifyed, deployDummySTOAndVerifyed, deploySignedTMAndVerifyed} from "./helpers/createInstances"; + +const DummySTO = artifacts.require("./DummySTO.sol"); +const SecurityToken = artifacts.require("./SecurityToken.sol"); +const GeneralTransferManager = artifacts.require("./GeneralTransferManager"); +const GeneralPermissionManager = artifacts.require("./GeneralPermissionManager"); +const SignedTransferManager = artifacts.require("./SignedTransferManager"); + +const Web3 = require("web3"); +const BigNumber = require("bignumber.js"); +const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); // Hardcoded development port + +contract("SignedTransferManager", accounts => { + // Accounts Variable declaration + let account_polymath; + let account_issuer; + let token_owner; + let token_owner_pk; + let account_investor1; + let account_investor2; + let account_investor3; + let account_investor4; + + // investor Details + let fromTime = latestTime(); + let toTime = latestTime(); + let expiryTime = toTime + duration.days(15); + + let message = "Transaction Should Fail!"; + + // Contract Instance Declaration + let I_GeneralPermissionManagerFactory; + let I_GeneralTransferManagerFactory; + let I_SecurityTokenRegistryProxy; + let I_GeneralPermissionManager; + let I_GeneralTransferManager; + let I_ModuleRegistryProxy; + let I_ModuleRegistry; + let I_FeatureRegistry; + let I_SecurityTokenRegistry; + let I_DummySTOFactory; + let I_STFactory; + let I_SecurityToken; + let I_STRProxied; + let I_MRProxied; + let I_DummySTO; + let I_PolyToken; + let I_PolymathRegistry; + let I_SignedTransferManagerFactory; + let P_SignedTransferManagerFactory; + let I_SignedTransferManager; + + // SecurityToken Details + const name = "Team"; + const symbol = "sap"; + const tokenDetails = "This is equity type of issuance"; + const decimals = 18; + const contact = "team@polymath.network"; + + // Module key + const delegateManagerKey = 1; + const transferManagerKey = 2; + const stoKey = 3; + + // Initial fee for ticker registry and security token registry + const initRegFee = web3.utils.toWei("250"); + + // Dummy STO details + const startTime = latestTime() + duration.seconds(5000); // Start time will be 5000 seconds more than the latest time + const endTime = startTime + duration.days(80); // Add 80 days more + const cap = web3.utils.toWei("10", "ether"); + const someString = "A string which is not used"; + const STOParameters = ["uint256", "uint256", "uint256", "string"]; + + before(async () => { + // Accounts setup + account_polymath = accounts[0]; + account_issuer = accounts[1]; + + token_owner = account_issuer; + token_owner_pk = pk.account_1; + + account_investor1 = accounts[8]; + account_investor2 = accounts[9]; + account_investor3 = accounts[6]; + account_investor4 = accounts[7]; + + // Step 1: Deploy the genral PM ecosystem + let instances = await setUpPolymathNetwork(account_polymath, token_owner); + + [ + I_PolymathRegistry, + I_PolyToken, + I_FeatureRegistry, + I_ModuleRegistry, + I_ModuleRegistryProxy, + I_MRProxied, + I_GeneralTransferManagerFactory, + I_STFactory, + I_SecurityTokenRegistry, + I_SecurityTokenRegistryProxy, + I_STRProxied + ] = instances; + + // STEP 2: Deploy the GeneralPermissionManagerFactory + [I_GeneralPermissionManagerFactory] = await deployGPMAndVerifyed(account_polymath, I_MRProxied, I_PolyToken.address, 0); + // STEP 3: Deploy the SignedTransferManagerFactory + [I_SignedTransferManagerFactory] = await deploySignedTMAndVerifyed(account_polymath, I_MRProxied, I_PolyToken.address, 0); + // STEP 4: Deploy the Paid SignedTransferManagerFactory + [P_SignedTransferManagerFactory] = await deploySignedTMAndVerifyed(account_polymath, I_MRProxied, I_PolyToken.address, web3.utils.toWei("500", "ether")); + + // Printing all the contract addresses + console.log(` + --------------------- Polymath Network Smart Contracts: --------------------- + PolymathRegistry: ${I_PolymathRegistry.address} + SecurityTokenRegistryProxy: ${I_SecurityTokenRegistryProxy.address} + SecurityTokenRegistry: ${I_SecurityTokenRegistry.address} + ModuleRegistryProxy: ${I_ModuleRegistryProxy.address} + ModuleRegistry: ${I_ModuleRegistry.address} + FeatureRegistry: ${I_FeatureRegistry.address} + + ManualApprovalTransferManagerFactory: ${I_SignedTransferManagerFactory.address} + + + ----------------------------------------------------------------------------- + `); + }); + + describe("Generate the SecurityToken", async () => { + it("Should register the ticker before the generation of the security token", async () => { + await I_PolyToken.approve(I_STRProxied.address, initRegFee, { from: token_owner }); + let tx = await I_STRProxied.registerTicker(token_owner, symbol, contact, { from: token_owner }); + assert.equal(tx.logs[0].args._owner, token_owner); + assert.equal(tx.logs[0].args._ticker, symbol.toUpperCase()); + }); + + it("Should generate the new security token with the same symbol as registered above", async () => { + 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 }); + + // Verify the successful generation of the security token + assert.equal(tx.logs[1].args._ticker, symbol.toUpperCase(), "SecurityToken doesn't get deployed"); + + I_SecurityToken = SecurityToken.at(tx.logs[1].args._securityTokenAddress); + + const log = await promisifyLogWatch(I_SecurityToken.ModuleAdded({ from: _blockNo }), 1); + + // Verify that GeneralTransferManager module get added successfully or not + assert.equal(log.args._types[0].toNumber(), 2); + assert.equal(web3.utils.toUtf8(log.args._name), "GeneralTransferManager"); + }); + + it("Should intialize the auto attached modules", async () => { + let moduleData = (await I_SecurityToken.getModulesByType(2))[0]; + I_GeneralTransferManager = GeneralTransferManager.at(moduleData); + }); + }); + + + describe("signed transfer manager tests", async () => { + + it("Should Buy the tokens", async () => { + // Add the Investor in to the whitelist + + let tx = await I_GeneralTransferManager.modifyWhitelist( + account_investor1, + latestTime(), + latestTime(), + latestTime() + duration.days(10), + true, + { + from: account_issuer, + gas: 6000000 + } + ); + + assert.equal( + tx.logs[0].args._investor.toLowerCase(), + account_investor1.toLowerCase(), + "Failed in adding the investor in whitelist" + ); + + // Jump time + await increaseTime(5000); + + // Mint some tokens + await I_SecurityToken.mint(account_investor1, web3.utils.toWei("4", "ether"), { from: token_owner }); + + assert.equal((await I_SecurityToken.balanceOf(account_investor1)).toNumber(), web3.utils.toWei("4", "ether")); + }); + + it("Should successfully attach the SignedTransferManager with the security token", async () => { + const tx = await I_SecurityToken.addModule(I_SignedTransferManagerFactory.address, "", 0, 0, { from: token_owner }); + assert.equal(tx.logs[2].args._types[0].toNumber(), transferManagerKey, "SignedTransferManager doesn't get deployed"); + assert.equal( + web3.utils.toUtf8(tx.logs[2].args._name), + "SignedTransferManager", + "SignedTransferManager module was not added" + ); + I_SignedTransferManager = SignedTransferManager.at(tx.logs[2].args._module); + }); + + it("should fail to transfer because transaction is not verified yet.", async () => { + await catchRevert(I_SecurityToken.transfer(account_investor2, web3.utils.toWei("1", "ether"), { from: account_investor1 })); + }); + + it("should successfully add multiple signers to signersList", async () => { + await I_SignedTransferManager.updateSigners([account_investor3, account_investor4, token_owner], [true, true, true], {from: token_owner}); + + assert.equal(await I_SignedTransferManager.signers(account_investor3), true); + assert.equal(await I_SignedTransferManager.signers(account_investor4), true); + assert.equal(await I_SignedTransferManager.signers(token_owner), true); + }); + + it("should fail to change signers stats without permission", async () => { + await catchRevert(I_SignedTransferManager.updateSigners([account_investor3], [false], {from: account_investor2})); + }); + + + it("should be able to invalid siganture if sender is the signer and is in the signer list", async () => { + const sig = signDataVerifyTransfer( + I_SignedTransferManager.address, + account_investor1, + account_investor2, + web3.utils.toWei("2", "ether"), + token_owner + ); + + console.log("token owner is "+ token_owner); + + await I_SignedTransferManager.invalidSignature(account_investor1, account_investor2, web3.utils.toWei("2", "ether"), sig, {from: token_owner}); + assert.equal(await I_SignedTransferManager.checkSignatureIsInvalid(sig), true); + }); + + it("should allow transfer with valid sig", async () => { + + console.log("owner is a signer status is " + await I_SignedTransferManager.signers(token_owner, {from: token_owner})); + + const sig = signDataVerifyTransfer( + I_SignedTransferManager.address, + account_investor1, + account_investor2, + web3.utils.toWei("1", "ether"), + token_owner + ); + + // let tx = await I_SignedTransferManager.verifyTransfer(account_investor1, account_investor2, web3.utils.toWei("1", "ether"), sig, false, {from: token_owner}); + console.log("owner token balance is " + (await I_SecurityToken.balanceOf(account_investor1)).toNumber()); + console.log("is this sig invalid?"+ await I_SignedTransferManager.checkSignatureIsInvalid(sig)); + + // test call security token transfer function + let tx = await I_SecurityToken.transferWithData(account_investor2, web3.utils.toWei("1", "ether"), sig, {from: account_investor1}); + console.log("3"); + assert.equal(await I_SignedTransferManager.checkSignatureIsInvalid(sig), true); + }); + + it("should not allow transfer if the sig is already used", async () => { + const sig = signDataVerifyTransfer( + I_SignedTransferManager.address, + account_investor1, + account_investor2, + web3.utils.toWei("1", "ether"), + token_owner + ); + + console.log("2"); + + await catchRevert (I_SignedTransferManager.verifyTransfer(account_investor1, account_investor2, web3.utils.toWei("1", "ether"), sig, false, {from: token_owner})); + }); + + it("should not allow transfer if the signer is not on the signer list", async () => { + const sig = signDataVerifyTransfer( + I_SignedTransferManager.address, + account_investor1, + account_investor2, + web3.utils.toWei("1", "ether"), + account_investor2 + ); + + let tx = await I_SignedTransferManager.verifyTransfer.call(account_investor1, account_investor2, web3.utils.toWei("1", "ether"), sig, false, {from: token_owner}); + console.log("output is "+tx.toNumber()); + + }); + + }); +}); +