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..4abb411f5 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"); + // new: exception for POL token + require( + registry.isTokenMapped(_token) || _token == registry.contractMap(keccak256("pol")), + "TOKEN_NOT_SUPPORTED" + ); _; } @@ -36,25 +43,48 @@ 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"))); + + // check that _amount is not too high + require(matic.balanceOf(address(this)) >= _amount, "amount exceeds this contract's MATIC balance"); + + // approve + matic.approve(registry.contractMap(keccak256("polygonMigration")), _amount); + + // call migrate function + IPolygonMigration(registry.contractMap(keccak256("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 +134,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 +161,19 @@ 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 { + // new: auto-migrate MATIC to POL + if (_token == registry.contractMap(keccak256("matic"))) { + _migrateMatic(_amountOrToken); + } + // new: bridge POL as MATIC, child chain behaviour does not change + else if (_token == registry.contractMap(keccak256("pol"))) { + _token = registry.contractMap(keccak256("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/migrations/4_initialize_state.js b/migrations/4_initialize_state.js index 7535a1eda..25f9c397b 100644 --- a/migrations/4_initialize_state.js +++ b/migrations/4_initialize_state.js @@ -1,6 +1,7 @@ const ethUtils = require('ethereumjs-util') const bluebird = require('bluebird') const utils = require('./utils') + const Registry = artifacts.require('Registry') const ValidatorShare = artifacts.require('ValidatorShare') const DepositManagerProxy = artifacts.require('DepositManagerProxy') @@ -16,13 +17,6 @@ const MaticWeth = artifacts.require('MaticWETH') const Governance = artifacts.require('Governance') const EventsHubProxy = artifacts.require('EventsHubProxy') -async function updateContractMap(governance, registry, nameHash, value) { - return governance.update( - registry.address, - registry.contract.methods.updateContractMap(nameHash, value).encodeABI() - ) -} - module.exports = async function(deployer) { deployer.then(async() => { const contractAddresses = utils.getContractAddresses() @@ -57,52 +51,52 @@ module.exports = async function(deployer) { TransferWithSigPredicate, EventsHubProxy ) { - await updateContractMap( + await utils.updateContractMap( governance, registry, - ethUtils.keccak256('validatorShare'), + 'validatorShare', validatorShare.address ) - await updateContractMap( + await utils.updateContractMap( governance, registry, - ethUtils.keccak256('depositManager'), + 'depositManager', depositManagerProxy.address ) - await updateContractMap( + await utils.updateContractMap( governance, registry, - ethUtils.keccak256('withdrawManager'), + 'withdrawManager', withdrawManagerProxy.address ) - await updateContractMap( + await utils.updateContractMap( governance, registry, - ethUtils.keccak256('stakeManager'), + 'stakeManager', stakeManagerProxy.address ) - await updateContractMap( + await utils.updateContractMap( governance, registry, - ethUtils.keccak256('slashingManager'), + 'slashingManager', slashingManager.address ) - await updateContractMap( + await utils.updateContractMap( governance, registry, - ethUtils.keccak256('stateSender'), + 'stateSender', stateSender.address ) - await updateContractMap( + await utils.updateContractMap( governance, registry, - ethUtils.keccak256('wethToken'), + 'wethToken', MaticWeth.address ) - await updateContractMap( + await utils.updateContractMap( governance, registry, - ethUtils.keccak256('eventsHub'), + 'eventsHub', EventsHubProxy.address ) diff --git a/migrations/7_pol_migration.js b/migrations/7_pol_migration.js new file mode 100644 index 000000000..87f9cbd34 --- /dev/null +++ b/migrations/7_pol_migration.js @@ -0,0 +1,118 @@ +const ethUtils = require('ethereumjs-util') +const assert = require('assert') + +const utils = require('./utils') +const contractAddresses = require('../contractAddresses.json') + +const DepositManager = artifacts.require('DepositManager') +const DepositManagerProxy = artifacts.require('DepositManagerProxy') + +const TestToken = artifacts.require('TestToken') +const POLTokenMock = artifacts.require('POLTokenMock') +const PolygonMigrationTest = artifacts.require('PolygonMigrationTest') +const Governance = artifacts.require('Governance') +const Registry = artifacts.require('Registry') + +async function deployPOLToken(governance, mintAmount) { + const registry = await Registry.at(contractAddresses.root.Registry) + + // Deploy POLToken. + const polToken = await POLTokenMock.new('Polygon Ecosystem Token', 'POL') + console.log('New POLToken deployed at', polToken.address) + + // Deploy PolygonMigration. + const polygonMigrationTest = await PolygonMigrationTest.new() + console.log('New PolygonMigration deployed at', polygonMigrationTest.address) + + // Map contracts in governance. + let result = await utils.updateContractMap(governance, registry, 'pol', polToken.address) + console.log('POLToken mapped in Governance:', result.tx) + + result = await utils.updateContractMap(governance, registry, 'polygonMigration', polygonMigrationTest.address) + console.log('PolygonMigration mapped in Governance:', result.tx) + + result = await utils.updateContractMap(governance, registry, 'matic', contractAddresses.root.tokens.MaticToken) + console.log('MaticToken mapped in Governance:', result.tx) + + // Set contract addresses in PolygonMigration. + result = await polygonMigrationTest.setTokenAddresses(contractAddresses.root.tokens.MaticToken, polToken.address) + console.log('PolygonMigration contract addresses (MATIC and POL) set:', result.tx) + + // Mint POL to PolygonMigration. + result = await polToken.mint(polygonMigrationTest.address, mintAmount) + console.log('POLToken minted to PolygonMigration:', result.tx) + + return { + polToken: polToken, + polygonMigration: polygonMigrationTest + } +} + +async function deployNewDepositManager(depositManagerProxy) { + const newDepositManager = await DepositManager.new() + console.log('New DepositManager deployed at', newDepositManager.address) + + const result = await depositManagerProxy.updateImplementation(newDepositManager.address) + console.log('Update DepositManagerProxy implementation:', result.tx) + + const implementation = await depositManagerProxy.implementation() + console.log('New implementation:', implementation) + return newDepositManager +} + +async function migrateMatic(governance, depositManager, mintAmount) { + // Mint MATIC to DepositManager. + const maticToken = await TestToken.at(contractAddresses.root.tokens.MaticToken) + let result = await maticToken.mint(depositManager.address, mintAmount) + console.log('MaticToken minted to DepositManager:', result.tx) + + // Migrate MATIC. + /* + // TODO: Understand why this call reverts. + result = await governance.update( + depositManager.address, + depositManager.contract.methods.migrateMatic(mintAmount).encodeABI() + ) + console.log('Migrate MATIC tokens to POL tokens:', result.tx) + + const newDepositManagerPOLBalance = await polToken.contract.methods.balanceOf(newDepositManager.address).call() + assertBigNumberEquality(newDepositManagerPOLBalance, maticAmountToMintAndMigrateInDepositManager) + */ +} + +function assertBigNumberEquality(num1, num2) { + if (!ethUtils.BN.isBN(num1)) num1 = web3.utils.toBN(num1.toString()) + if (!ethUtils.BN.isBN(num2)) num2 = web3.utils.toBN(num2.toString()) + assert( + num1.eq(num2), + `expected ${num1.toString(10)} and ${num2.toString(10)} to be equal` + ) +} + +module.exports = async function(deployer, _, _) { + deployer.then(async() => { + const oneEther = web3.utils.toBN('10').pow(web3.utils.toBN('18')) + + // Deploy contracts. + console.log('> Deploying POL token contracts...') + const governance = await Governance.at(contractAddresses.root.GovernanceProxy) + const polTokenAmountInMigrationContract = oneEther.mul(web3.utils.toBN('1000000000000000000')).toString() + const { polToken, polygonMigration } = await deployPOLToken(governance, polTokenAmountInMigrationContract) + + console.log('\n> Updating DepositManager...') + const depositManagerProxy = await DepositManagerProxy.at(contractAddresses.root.DepositManagerProxy) + const newDepositManager = await deployNewDepositManager(depositManagerProxy) + + // Migrate MATIC. + console.log('\n> Migrating MATIC to POL...') + const maticAmountToMintAndMigrateInDepositManager = oneEther.mul(web3.utils.toBN('1000000000')).toString() // 100 ethers + await migrateMatic(governance, newDepositManager, maticAmountToMintAndMigrateInDepositManager) + + // Update contract addresses. + contractAddresses.root.NewDepositManager = newDepositManager.address + contractAddresses.root.PolToken = polToken.address + contractAddresses.root.PolygonMigration = polygonMigration.address + utils.writeContractAddresses(contractAddresses) + console.log('\n> Contract addresses updated!') + }) +} diff --git a/migrations/utils.js b/migrations/utils.js index 6457f02c7..f369a836a 100644 --- a/migrations/utils.js +++ b/migrations/utils.js @@ -1,4 +1,5 @@ const fs = require('fs') +const ethUtils = require('ethereumjs-util') export function getContractAddresses() { return JSON.parse(fs.readFileSync(`${process.cwd()}/contractAddresses.json`).toString()) @@ -10,3 +11,10 @@ export function writeContractAddresses(contractAddresses) { JSON.stringify(contractAddresses, null, 2) // Indent 2 spaces ) } + +export async function updateContractMap(governance, registry, name, value) { + return governance.update( + registry.address, + registry.contract.methods.updateContractMap(ethUtils.keccak256(name), value).encodeABI() + ) +} diff --git a/package-lock.json b/package-lock.json index b1283138d..6bae4d586 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11000,9 +11000,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.597", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.597.tgz", - "integrity": "sha512-0XOQNqHhg2YgRVRUrS4M4vWjFCFIP2ETXcXe/0KIQBjXE9Cpy+tgzzYfuq6HGai3hWq0YywtG+5XK8fyG08EjA==" + "version": "1.4.600", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.600.tgz", + "integrity": "sha512-KD6CWjf1BnQG+NsXuyiTDDT1eV13sKuYsOUioXkQweYTQIbgHkXPry9K7M+7cKtYHnSUPitVaLrXYB1jTkkYrw==" }, "node_modules/elliptic": { "version": "6.3.3", @@ -20190,9 +20190,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/nofilter": { "version": "1.0.4", @@ -37073,9 +37073,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { - "version": "1.4.597", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.597.tgz", - "integrity": "sha512-0XOQNqHhg2YgRVRUrS4M4vWjFCFIP2ETXcXe/0KIQBjXE9Cpy+tgzzYfuq6HGai3hWq0YywtG+5XK8fyG08EjA==" + "version": "1.4.600", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.600.tgz", + "integrity": "sha512-KD6CWjf1BnQG+NsXuyiTDDT1eV13sKuYsOUioXkQweYTQIbgHkXPry9K7M+7cKtYHnSUPitVaLrXYB1jTkkYrw==" }, "elliptic": { "version": "6.3.3", @@ -44565,9 +44565,9 @@ } }, "node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "nofilter": { "version": "1.0.4",