diff --git a/contracts/schemes/TransitionScheme.sol b/contracts/schemes/TransitionScheme.sol new file mode 100644 index 00000000..fd28db21 --- /dev/null +++ b/contracts/schemes/TransitionScheme.sol @@ -0,0 +1,100 @@ +pragma solidity 0.5.17; + +import "../controller/Controller.sol"; + +/** + * @title A scheme for transitioning the DAO's assets to a new one. + */ + +contract TransitionScheme { + + uint256 public constant ASSETS_CAP = 100; + + event OwnershipTransferred(Avatar indexed _avatar, address indexed _newAvatar, address indexed _asset); + + Avatar public avatar; + address payable public newAvatar; + address[] public externalTokens; + address[] public assetAddresses; + bytes4[] public selectors; + + /** + * @dev initialize + * @param _avatar the avatar to migrate from + * @param _newAvatar the avatar to migrate to + * @param _externalTokens external tokens to allow transfer to the new avatar + * @param _assetAddresses the assets to transfer + * @param _selectors the functions to call to to transfer the assets + */ + function initialize( + Avatar _avatar, + address payable _newAvatar, + address[] calldata _externalTokens, + address[] calldata _assetAddresses, + bytes4[] calldata _selectors + ) external { + require(_assetAddresses.length <= ASSETS_CAP, "cannot transfer more than 100 assets"); + require(_assetAddresses.length == _selectors.length, "Arrays length mismatch"); + require(avatar == Avatar(0), "can be called only one time"); + require(_avatar != Avatar(0), "avatar cannot be zero"); + avatar = _avatar; + newAvatar = _newAvatar; + externalTokens = _externalTokens; + assetAddresses = _assetAddresses; + selectors = _selectors; + } + + /** + * @dev transferAssets function + * transfer the DAO assets to a new DAO + */ + function transferAssets() external { + for (uint256 i=0; i < assetAddresses.length; i++) { + bytes memory genericCallReturnValue; + bool success; + Controller controller = Controller(avatar.owner()); + (success, genericCallReturnValue) = + controller.genericCall( + assetAddresses[i], + abi.encodeWithSelector(selectors[i], newAvatar), + avatar, + 0 + ); + if (success) { + emit OwnershipTransferred(avatar, newAvatar, assetAddresses[i]); + } + } + } + + /** + * @dev sendEther function + * @param _amount the amount of ether to send to the new avatar + */ + function sendEther(uint256 _amount) external { + require( + Controller(avatar.owner()).sendEther(_amount, newAvatar, avatar), + "Sending ether should succeed" + ); + } + + /** + * @dev sendExternalToken function + * @param _amounts the amounts of tokens to send to the new avatar + */ + function sendExternalToken(uint256[] calldata _amounts) external { + require(externalTokens.length == _amounts.length, "Arrays length mismatch"); + for (uint256 i=0; i < externalTokens.length; i++) { + if (_amounts[i] > 0) { + require( + Controller(avatar.owner()).externalTokenTransfer( + IERC20(externalTokens[i]), + newAvatar, + _amounts[i], + avatar + ), + "Sending external token should succeed" + ); + } + } + } +} diff --git a/package-lock.json b/package-lock.json index ae83e662..2d81c9f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@daostack/arc", - "version": "0.0.1-rc.41", + "version": "0.0.1-rc.42", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -221,9 +221,9 @@ "integrity": "sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ==" }, "acorn": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", - "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", "dev": true }, "acorn-globals": { @@ -4683,9 +4683,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, "lodash.findindex": { @@ -4837,9 +4837,9 @@ } }, "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "mixin-deep": { "version": "1.3.2", @@ -4863,11 +4863,11 @@ } }, "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "requires": { - "minimist": "0.0.8" + "minimist": "^1.2.5" } }, "mocha": { @@ -4918,6 +4918,21 @@ "path-is-absolute": "^1.0.0" } }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -5404,6 +5419,21 @@ "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", "dev": true }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/package.json b/package.json index 1f9593c8..9213c62e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@daostack/arc", - "version": "0.0.1-rc.41", + "version": "0.0.1-rc.42", "description": "A platform for building DAOs", "files": [ "contracts/", diff --git a/test/transitionscheme.js b/test/transitionscheme.js new file mode 100644 index 00000000..a86d1b7c --- /dev/null +++ b/test/transitionscheme.js @@ -0,0 +1,208 @@ +const helpers = require('./helpers'); +const DaoCreator = artifacts.require('./DaoCreator.sol'); +const ControllerCreator = artifacts.require('./ControllerCreator.sol'); +const DAOTracker = artifacts.require('./DAOTracker.sol'); +const constants = require('./constants'); +const ERC20Mock = artifacts.require('./test/ERC20Mock.sol'); +const Wallet = artifacts.require("./Wallet.sol"); +const TransitionScheme = artifacts.require('./TransitionScheme.sol'); + +let selector = web3.eth.abi.encodeFunctionSignature('transferOwnership(address)'); + +const setup = async function( + accounts, + testInitDifferentArrayLength=false, + testLimit=false, + testOverLimit=false, +) { + var testSetup = new helpers.TestSetup(); + var controllerCreator = await ControllerCreator.new({ + gas: constants.ARC_GAS_LIMIT + }); + var daoTracker = await DAOTracker.new({ gas: constants.ARC_GAS_LIMIT }); + testSetup.daoCreator = await DaoCreator.new( + controllerCreator.address, + daoTracker.address, + { gas: constants.ARC_GAS_LIMIT } + ); + + testSetup.org = await helpers.setupOrganization( + testSetup.daoCreator, + accounts[0], + 1000, + 1000 + ); + + testSetup.wallet = await Wallet.new(); + await testSetup.wallet.transferOwnership(testSetup.org.avatar.address); + testSetup.assets = [testSetup.wallet.address]; + + testSetup.standardToken = await ERC20Mock.new(testSetup.org.avatar.address, 100); + + testSetup.transitionScheme = await TransitionScheme.new(); + testSetup.selectors = [selector]; + if (testInitDifferentArrayLength) { + testSetup.selectors = [selector, selector]; + } + + if (testLimit) { + for (let i=0; i < (testOverLimit ? 100 : 99); i++) { + let wallet = await Wallet.new(); + await wallet.transferOwnership(testSetup.org.avatar.address); + testSetup.assets.push(wallet.address); + testSetup.selectors.push(selector); + } + } + + await testSetup.transitionScheme.initialize( + testSetup.org.avatar.address, + helpers.SOME_ADDRESS, + [testSetup.standardToken.address], + testSetup.assets, + testSetup.selectors, + { gas: constants.ARC_GAS_LIMIT } + ); + + var permissions = '0x00000010'; + await testSetup.daoCreator.setSchemes( + testSetup.org.avatar.address, + [testSetup.transitionScheme.address], + [web3.utils.asciiToHex('0')], + [permissions], + 'metaData' + ); + + return testSetup; +}; + +contract('TransitionScheme', accounts => { + it('initialize', async () => { + let testSetup = await setup(accounts); + + assert.equal( + await testSetup.transitionScheme.avatar(), + testSetup.org.avatar.address + ); + assert.equal( + await testSetup.transitionScheme.newAvatar(), + helpers.SOME_ADDRESS + ); + assert.equal( + await testSetup.transitionScheme.externalTokens(0), + testSetup.standardToken.address + ); + assert.equal( + await testSetup.transitionScheme.assetAddresses(0), + testSetup.wallet.address + ); + assert.equal(await testSetup.transitionScheme.selectors(0), selector); + }); + + it('initialize assets and selector arrays must be same length', async () => { + try { + await setup(accounts, true); + assert(false, 'assets and selector arrays must be same length'); + } catch (error) { + helpers.assertVMException(error); + } + }); + + it('initialize with more than 100 assets should fail', async () => { + try { + await setup(accounts, false, true, true); + assert(false, 'initialize with more than 100 assets should fail'); + } catch (error) { + helpers.assertVMException(error); + } + }); + + it('transfer assets', async () => { + let testSetup = await setup(accounts); + assert.equal(await testSetup.wallet.owner(), testSetup.org.avatar.address); + let tx = await testSetup.transitionScheme.transferAssets(); + await testSetup.transitionScheme.getPastEvents('OwnershipTransferred', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events[0].event,"OwnershipTransferred"); + assert.equal(events[0].args._avatar, testSetup.org.avatar.address); + assert.equal(events[0].args._newAvatar, helpers.SOME_ADDRESS); + assert.equal(events[0].args._asset, testSetup.wallet.address); + }); + assert.equal(await testSetup.wallet.owner(), helpers.SOME_ADDRESS); + }); + + it('transfer many assets', async () => { + let testSetup = await setup(accounts, false, true); + assert.equal(await testSetup.wallet.owner(), testSetup.org.avatar.address); + let tx = await testSetup.transitionScheme.transferAssets(); + await testSetup.transitionScheme.getPastEvents('OwnershipTransferred', { + fromBlock: tx.blockNumber, + toBlock: 'latest' + }) + .then(function(events){ + assert.equal(events.length, 100); + assert.equal(events[0].event,"OwnershipTransferred"); + assert.equal(events[0].args._avatar, testSetup.org.avatar.address); + assert.equal(events[0].args._newAvatar, helpers.SOME_ADDRESS); + assert.equal(events[0].args._asset, testSetup.wallet.address); + }); + assert.equal(await testSetup.wallet.owner(), helpers.SOME_ADDRESS); + }); + + it('transfer avatar ether', async () => { + let testSetup = await setup(accounts); + await web3.eth.sendTransaction({from:accounts[0],to: testSetup.org.avatar.address, value: web3.utils.toWei('1', 'ether')}); + assert.equal(await web3.eth.getBalance(testSetup.org.avatar.address),web3.utils.toWei('1', "ether")); + assert.equal(await web3.eth.getBalance(helpers.SOME_ADDRESS), 0); + await testSetup.transitionScheme.sendEther(web3.utils.toWei('1', 'ether')); + assert.equal(await web3.eth.getBalance(testSetup.org.avatar.address), 0); + assert.equal(await web3.eth.getBalance(helpers.SOME_ADDRESS),web3.utils.toWei('1', "ether")); + + await web3.eth.sendTransaction({from:accounts[0],to: testSetup.org.avatar.address, value: web3.utils.toWei('1', 'ether')}); + assert.equal(await web3.eth.getBalance(testSetup.org.avatar.address),web3.utils.toWei('1', "ether")); + assert.equal(await web3.eth.getBalance(helpers.SOME_ADDRESS),web3.utils.toWei('1', "ether")); + await testSetup.transitionScheme.sendEther(web3.utils.toWei('1', 'ether')); + assert.equal(await web3.eth.getBalance(testSetup.org.avatar.address), 0); + assert.equal(await web3.eth.getBalance(helpers.SOME_ADDRESS),web3.utils.toWei('2', "ether")); + }); + + it('transfer avatar external tokens', async () => { + let testSetup = await setup(accounts); + assert.equal(await testSetup.standardToken.balanceOf(testSetup.org.avatar.address), 100); + assert.equal(await testSetup.standardToken.balanceOf(helpers.SOME_ADDRESS), 0); + await testSetup.transitionScheme.sendExternalToken([100]); + assert.equal(await testSetup.standardToken.balanceOf(testSetup.org.avatar.address), 0); + assert.equal(await testSetup.standardToken.balanceOf(helpers.SOME_ADDRESS), 100); + }); + + it('external tokens and amounts arrays must be same length', async () => { + let testSetup = await setup(accounts); + assert.equal(await testSetup.standardToken.balanceOf(testSetup.org.avatar.address), 100); + assert.equal(await testSetup.standardToken.balanceOf(helpers.SOME_ADDRESS), 0); + try { + await testSetup.transitionScheme.sendExternalToken([100, 10]); + assert(false, 'external tokens and amounts arrays must be same length'); + } catch (error) { + helpers.assertVMException(error); + } + }); + + it('cannot initialize twice', async () => { + let testSetup = await setup(accounts); + try { + await testSetup.transitionScheme.initialize( + testSetup.org.avatar.address, + helpers.SOME_ADDRESS, + [testSetup.standardToken.address], + [testSetup.wallet.address], + testSetup.selectors, + { gas: constants.ARC_GAS_LIMIT } + ); + assert(false, 'cannot initialize twice'); + } catch (error) { + helpers.assertVMException(error); + } + }); +});