diff --git a/.gitignore b/.gitignore index 12942d270..9febdea32 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ test-blockchain/data *.log .DS_Store +.vscode *.env coverage/ diff --git a/.soliumrc.json b/.soliumrc.json index 03d5f6097..264b4a602 100644 --- a/.soliumrc.json +++ b/.soliumrc.json @@ -14,7 +14,7 @@ "no-unused-vars": 1, "quotes": 1, "error-reason": 0, - "indentation": ["error", 2], + "indentation": ["error", 4], "arg-overflow": ["error", 8], "whitespace": 1, "deprecated-suicide": 1, diff --git a/contracts/common/tokens/POLTokenMock.sol b/contracts/common/tokens/POLTokenMock.sol new file mode 100644 index 000000000..c666f98a5 --- /dev/null +++ b/contracts/common/tokens/POLTokenMock.sol @@ -0,0 +1,56 @@ +pragma solidity ^0.5.2; + +import "openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol"; +import "openzeppelin-solidity/contracts/utils/Address.sol"; + + +contract POLTokenMock is ERC20Mintable { + using Address for address; + + // detailed ERC20 + string public name; + string public symbol; + uint8 public decimals = 18; + + constructor(string memory _name, string memory _symbol) public { + name = _name; + symbol = _symbol; + + uint256 value = 10**10 * (10**18); + mint(msg.sender, value); + } + + function safeTransfer(IERC20 token, address to, uint256 value) public { + callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); + } + + function safeTransferFrom(IERC20 token, address from, address to, uint256 value) public { + callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must equal true). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + */ + function callOptionalReturn(IERC20 token, bytes memory data) private { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. + + // A Solidity high level call has three parts: + // 1. The target address is checked to verify it contains contract code + // 2. The call itself is made, and success asserted + // 3. The return value is decoded, which in turn checks the size of the returned data. + + require(address(token).isContract()); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = address(token).call(data); + require(success); + + if (returndata.length > 0) { // Return data is optional + require(abi.decode(returndata, (bool))); + } + } +} diff --git a/contracts/root/depositManager/DepositManager.sol b/contracts/root/depositManager/DepositManager.sol index b3be23ad2..0cf6ac2b1 100644 --- a/contracts/root/depositManager/DepositManager.sol +++ b/contracts/root/depositManager/DepositManager.sol @@ -14,13 +14,20 @@ import {StateSender} from "../stateSyncer/StateSender.sol"; import {GovernanceLockable} from "../../common/mixin/GovernanceLockable.sol"; import {RootChain} from "../RootChain.sol"; +interface IPolygonMigration { + function migrate(uint256 amount) external; +} + contract DepositManager is DepositManagerStorage, IDepositManager, ERC721Holder { using SafeMath for uint256; using SafeERC20 for IERC20; modifier isTokenMapped(address _token) { - require(registry.isTokenMapped(_token), "TOKEN_NOT_SUPPORTED"); + require( + registry.isTokenMapped(_token), + "TOKEN_NOT_SUPPORTED" + ); _; } @@ -36,25 +43,49 @@ contract DepositManager is DepositManagerStorage, IDepositManager, ERC721Holder depositEther(); } + // new: governance function to migrate MATIC to POL + function migrateMatic(uint256 _amount) external onlyGovernance { + _migrateMatic(_amount); + } + + function _migrateMatic(uint256 _amount) private { + IERC20 matic = IERC20(registry.contractMap(keccak256("matic"))); + address polygonMigration = registry.contractMap(keccak256("polygonMigration")); + + // check that _amount is not too high + require(matic.balanceOf(address(this)) >= _amount, "amount exceeds this contract's MATIC balance"); + + // approve + matic.approve(polygonMigration, _amount); + + // call migrate function + IPolygonMigration(polygonMigration).migrate(_amount); + } + function updateMaxErc20Deposit(uint256 maxDepositAmount) public onlyGovernance { require(maxDepositAmount != 0); emit MaxErc20DepositUpdate(maxErc20Deposit, maxDepositAmount); maxErc20Deposit = maxDepositAmount; } - function transferAssets( - address _token, - address _user, - uint256 _amountOrNFTId - ) external isPredicateAuthorized { + function transferAssets(address _token, address _user, uint256 _amountOrNFTId) external isPredicateAuthorized { address wethToken = registry.getWethTokenAddress(); + if (registry.isERC721(_token)) { IERC721(_token).transferFrom(address(this), _user, _amountOrNFTId); } else if (_token == wethToken) { WETH t = WETH(_token); t.withdraw(_amountOrNFTId, _user); } else { - require(IERC20(_token).transfer(_user, _amountOrNFTId), "TRANSFER_FAILED"); + // new: pay out POL when MATIC is withdrawn + if (_token == registry.contractMap(keccak256("matic"))) { + require( + IERC20(registry.contractMap(keccak256("pol"))).transfer(_user, _amountOrNFTId), + "TRANSFER_FAILED" + ); + } else { + require(IERC20(_token).transfer(_user, _amountOrNFTId), "TRANSFER_FAILED"); + } } } @@ -104,21 +135,14 @@ contract DepositManager is DepositManagerStorage, IDepositManager, ERC721Holder stateSender = StateSender(_stateSender); } - function depositERC20ForUser( - address _token, - address _user, - uint256 _amount - ) public { + function depositERC20ForUser(address _token, address _user, uint256 _amount) public { require(_amount <= maxErc20Deposit, "exceed maximum deposit amount"); IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); + _safeCreateDepositBlock(_user, _token, _amount); } - function depositERC721ForUser( - address _token, - address _user, - uint256 _tokenId - ) public { + function depositERC721ForUser(address _token, address _user, uint256 _tokenId) public { require(registry.isTokenMappedAndIsErc721(_token), "not erc721"); _safeTransferERC721(msg.sender, _token, _tokenId); @@ -138,20 +162,21 @@ contract DepositManager is DepositManagerStorage, IDepositManager, ERC721Holder address _token, uint256 _amountOrToken ) internal onlyWhenUnlocked isTokenMapped(_token) { - _createDepositBlock( - _user, - _token, - _amountOrToken, - rootChain.updateDepositId(1) /* returns _depositId */ - ); + _createDepositBlock(_user, _token, _amountOrToken, rootChain.updateDepositId(1)); // returns _depositId } - function _createDepositBlock( - address _user, - address _token, - uint256 _amountOrToken, - uint256 _depositId - ) internal { + function _createDepositBlock(address _user, address _token, uint256 _amountOrToken, uint256 _depositId) internal { + address matic = registry.contractMap(keccak256("matic")); + + // new: auto-migrate MATIC to POL + if (_token == matic) { + _migrateMatic(_amountOrToken); + } + // new: bridge POL as MATIC, child chain behaviour does not change + else if (_token == registry.contractMap(keccak256("pol"))) { + _token = matic; + } + deposits[_depositId] = DepositBlock(keccak256(abi.encodePacked(_user, _token, _amountOrToken)), now); stateSender.syncState(childChain, abi.encode(_user, _token, _amountOrToken, _depositId)); emit NewDepositBlock(_user, _token, _amountOrToken, _depositId); diff --git a/contracts/test/PolygonMigrationTest.sol b/contracts/test/PolygonMigrationTest.sol new file mode 100644 index 000000000..6089be8ce --- /dev/null +++ b/contracts/test/PolygonMigrationTest.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.5.2; + +import {IERC20} from "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "openzeppelin-solidity/contracts/token/ERC20/SafeERC20.sol"; + +// this impl was shortened for testing purposes +// full impl at https://github.com/0xPolygon/indicia/blob/main/src/PolygonMigration.sol +contract PolygonMigrationTest { + using SafeERC20 for IERC20; + + event Migrated(address indexed account, uint256 amount); + + IERC20 public polygon; + IERC20 public matic; + + function setTokenAddresses(address matic_, address polygon_) external { + if (matic_ == address(0)) revert(); + matic = IERC20(matic_); + + if (polygon_ == address(0)) revert(); + polygon = IERC20(polygon_); + } + + /// @notice This function allows for migrating MATIC tokens to POL tokens + /// @dev The function does not do any validation since the migration is a one-way process + /// @param amount Amount of MATIC to migrate + function migrate(uint256 amount) external { + emit Migrated(msg.sender, amount); + + matic.safeTransferFrom(msg.sender, address(this), amount); + polygon.safeTransfer(msg.sender, amount); + } +} diff --git a/package-lock.json b/package-lock.json index e6e96d4cf..170cbe8a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1219,6 +1219,20 @@ "underscore": "1.9.1", "web3-core-helpers": "1.2.1", "websocket": "github:web3-js/WebSocket-Node#ef5ea2f41daf4a2113b80c9223df884b4d56c400" + }, + "dependencies": { + "websocket": { + "version": "github:web3-js/WebSocket-Node#ef5ea2f41daf4a2113b80c9223df884b4d56c400", + "from": "github:web3-js/WebSocket-Node#polyfill/globalThis", + "dev": true, + "requires": { + "debug": "^2.2.0", + "es5-ext": "^0.10.50", + "nan": "^2.14.0", + "typedarray-to-buffer": "^3.1.5", + "yaeti": "^0.0.6" + } + } } }, "web3-shh": { @@ -1236,7 +1250,6 @@ "websocket": { "version": "github:web3-js/WebSocket-Node#ef5ea2f41daf4a2113b80c9223df884b4d56c400", "from": "github:web3-js/WebSocket-Node#polyfill/globalThis", - "dev": true, "requires": { "debug": "^2.2.0", "es5-ext": "^0.10.50", @@ -1705,6 +1718,20 @@ "underscore": "1.9.1", "web3-core-helpers": "1.2.1", "websocket": "github:web3-js/WebSocket-Node#ef5ea2f41daf4a2113b80c9223df884b4d56c400" + }, + "dependencies": { + "websocket": { + "version": "github:web3-js/WebSocket-Node#ef5ea2f41daf4a2113b80c9223df884b4d56c400", + "from": "github:web3-js/WebSocket-Node#polyfill/globalThis", + "dev": true, + "requires": { + "debug": "^2.2.0", + "es5-ext": "^0.10.50", + "nan": "^2.14.0", + "typedarray-to-buffer": "^3.1.5", + "yaeti": "^0.0.6" + } + } } }, "web3-shh": { @@ -1722,7 +1749,6 @@ "websocket": { "version": "github:web3-js/WebSocket-Node#ef5ea2f41daf4a2113b80c9223df884b4d56c400", "from": "github:web3-js/WebSocket-Node#polyfill/globalThis", - "dev": true, "requires": { "debug": "^2.2.0", "es5-ext": "^0.10.50", @@ -1786,6 +1812,14 @@ "integrity": "sha512-yzBInQFhdY8kaZmqoL2+3U5dSTMrKaYcb561VU+lDzAYvqt+2lojvBEy+hmpSNuXnPTx7m9+04CzWYOUqWME2A==", "dev": true }, + "@web3-js/scrypt-shim": { + "version": "github:web3-js/scrypt-shim#aafdadda13e660e25e1c525d1f5b2443f5eb1ebb", + "from": "github:web3-js/scrypt-shim#aafdadda13e660e25e1c525d1f5b2443f5eb1ebb", + "requires": { + "scryptsy": "^2.1.0", + "semver": "^6.3.0" + } + }, "elliptic": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", @@ -2191,6 +2225,20 @@ "underscore": "1.9.1", "web3-core-helpers": "1.2.1", "websocket": "github:web3-js/WebSocket-Node#ef5ea2f41daf4a2113b80c9223df884b4d56c400" + }, + "dependencies": { + "websocket": { + "version": "github:web3-js/WebSocket-Node#ef5ea2f41daf4a2113b80c9223df884b4d56c400", + "from": "github:web3-js/WebSocket-Node#polyfill/globalThis", + "dev": true, + "requires": { + "debug": "^2.2.0", + "es5-ext": "^0.10.50", + "nan": "^2.14.0", + "typedarray-to-buffer": "^3.1.5", + "yaeti": "^0.0.6" + } + } } }, "web3-shh": { @@ -2219,6 +2267,17 @@ "underscore": "1.9.1", "utf8": "3.0.0" } + }, + "websocket": { + "version": "github:web3-js/WebSocket-Node#ef5ea2f41daf4a2113b80c9223df884b4d56c400", + "from": "github:web3-js/WebSocket-Node#ef5ea2f41daf4a2113b80c9223df884b4d56c400", + "requires": { + "debug": "^2.2.0", + "es5-ext": "^0.10.50", + "nan": "^2.14.0", + "typedarray-to-buffer": "^3.1.5", + "yaeti": "^0.0.6" + } } } }, @@ -2588,7 +2647,6 @@ "websocket": { "version": "github:web3-js/WebSocket-Node#ef5ea2f41daf4a2113b80c9223df884b4d56c400", "from": "github:web3-js/WebSocket-Node#polyfill/globalThis", - "dev": true, "requires": { "debug": "^2.2.0", "es5-ext": "^0.10.50", @@ -5913,7 +5971,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "requires": { "ms": "2.0.0" } @@ -8554,7 +8611,7 @@ "get-func-name": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", "dev": true }, "get-intrinsic": { @@ -11149,9 +11206,9 @@ } }, "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true }, "pbkdf2": { @@ -15014,7 +15071,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, "requires": { "is-typedarray": "^1.0.0" } @@ -16399,8 +16455,7 @@ "yaeti": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", - "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=", - "dev": true + "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=" }, "yallist": { "version": "3.1.1", diff --git a/package.json b/package.json index 45f62a3db..e9920b485 100644 --- a/package.json +++ b/package.json @@ -70,9 +70,9 @@ "solidity-coverage": "^0.7.4", "solium": "^1.1.8", "truffle": "5.0.34", + "truffle-contract-size": "^1.0.1", "web3": "^1.0.0-beta.33", - "web3-eth-abi": "^1.0.0-beta.51", - "truffle-contract-size": "^1.0.1" + "web3-eth-abi": "^1.0.0-beta.51" }, "dependencies": { "@truffle/hdwallet-provider": "^1.4.0", diff --git a/scripts/updateDepositManager.js b/scripts/updateDepositManager.js new file mode 100644 index 000000000..2c37a92c0 --- /dev/null +++ b/scripts/updateDepositManager.js @@ -0,0 +1,34 @@ +const contractAddresses = require('../contractAddresses.json') + +const DepositManager = artifacts.require('DepositManager') +const DepositManagerProxy = artifacts.require('DepositManagerProxy') + +async function deployAndUpdateWM() { + const newDM = await DepositManager.new() + console.log('new DM deployed at: ', newDM.address) + const dmProxy = await DepositManagerProxy.at( + contractAddresses.root.DepositManagerProxy + ) + const result = await dmProxy.updateImplementation(newDM.address) + console.log('tx hash: ', result.tx) + + const impl = await dmProxy.implementation() + console.log('new impl: ', impl) +} + +module.exports = async function(callback) { + // args starts with index 6, example: first arg == process.args[6] + console.log(process.argv) + try { + const accounts = await web3.eth.getAccounts() + console.log( + 'Current configured address to make transactions:', + accounts[0] + ) + await deployAndUpdateWM() + } catch (e) { + // truffle exec