From e041d7615b2c2343aa1445ebd0751ff8c20cbcef Mon Sep 17 00:00:00 2001 From: Simon Dosch Date: Mon, 18 Sep 2023 11:25:37 +0200 Subject: [PATCH] new behaviour for POL integration --- contracts/common/tokens/TestToken.sol | 37 +++++ .../root/depositManager/DepositManager.sol | 41 +++-- contracts/test/PolygonMigrationTest.sol | 34 ++++ package-lock.json | 43 +++-- package.json | 4 +- test/helpers/artifacts.js | 1 + .../root/DepositManagerUpdate.test.js | 152 ++++++++++++++++++ truffle-config.js | 2 +- 8 files changed, 288 insertions(+), 26 deletions(-) create mode 100644 contracts/test/PolygonMigrationTest.sol create mode 100644 test/integration/root/DepositManagerUpdate.test.js diff --git a/contracts/common/tokens/TestToken.sol b/contracts/common/tokens/TestToken.sol index 7dffc006..7996ee73 100644 --- a/contracts/common/tokens/TestToken.sol +++ b/contracts/common/tokens/TestToken.sol @@ -1,8 +1,11 @@ pragma solidity ^0.5.2; import "openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol"; +import "openzeppelin-solidity/contracts/utils/Address.sol"; contract TestToken is ERC20Mintable { + using Address for address; + // detailed ERC20 string public name; string public symbol; @@ -15,4 +18,38 @@ contract TestToken is ERC20Mintable { 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 a12e121e..6a3d088d 100644 --- a/contracts/root/depositManager/DepositManager.sol +++ b/contracts/root/depositManager/DepositManager.sol @@ -18,12 +18,14 @@ 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"); _; } @@ -39,6 +41,7 @@ contract DepositManager is DepositManagerStorage, IDepositManager, ERC721Holder depositEther(); } + // new: governance function to migrate MATIC to POL function migrateMatic(uint256 _amount) external onlyGovernance { _migrateMatic(_amount); } @@ -47,7 +50,7 @@ contract DepositManager is DepositManagerStorage, IDepositManager, ERC721Holder 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"); + require(matic.balanceOf(address(this)) >= _amount, "amount exceeds this contract's MATIC balance"); // approve matic.approve(registry.contractMap(keccak256("polygonMigration")), _amount); @@ -64,13 +67,22 @@ contract DepositManager is DepositManagerStorage, IDepositManager, ERC721Holder 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); + + // so we don't assign to a function var + address memory token = _token; + + // new: pay out POL when MATIC is withdrawn + if (_token == registry.contractMap(keccak256("matic"))) { + token = registry.contractMap(keccak256("pol")); + } + + 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"); + require(IERC20(token).transfer(_user, _amountOrNFTId), "TRANSFER_FAILED"); } } @@ -124,11 +136,6 @@ contract DepositManager is DepositManagerStorage, IDepositManager, ERC721Holder require(_amount <= maxErc20Deposit, "exceed maximum deposit amount"); IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); - // new: auto-migrate MATIC to POL - if (_token == registry.contractMap(keccak256("matic"))) { - _migrateMatic(_amount); - } - _safeCreateDepositBlock(_user, _token, _amount); } @@ -156,6 +163,16 @@ contract DepositManager is DepositManagerStorage, IDepositManager, ERC721Holder } 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 + 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 00000000..6089be8c --- /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 9cfd67a8..170cbe8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1217,12 +1217,14 @@ "dev": true, "requires": { "underscore": "1.9.1", - "web3-core-helpers": "1.2.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#ef5ea2f41daf4a2113b80c9223df884b4d56c400", + "from": "github:web3-js/WebSocket-Node#polyfill/globalThis", + "dev": true, "requires": { "debug": "^2.2.0", "es5-ext": "^0.10.50", @@ -1714,12 +1716,14 @@ "dev": true, "requires": { "underscore": "1.9.1", - "web3-core-helpers": "1.2.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#ef5ea2f41daf4a2113b80c9223df884b4d56c400", + "from": "github:web3-js/WebSocket-Node#polyfill/globalThis", + "dev": true, "requires": { "debug": "^2.2.0", "es5-ext": "^0.10.50", @@ -2219,7 +2223,22 @@ "dev": true, "requires": { "underscore": "1.9.1", - "web3-core-helpers": "1.2.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": { @@ -2571,12 +2590,14 @@ "dev": true, "requires": { "underscore": "1.9.1", - "web3-core-helpers": "1.2.2" + "web3-core-helpers": "1.2.2", + "websocket": "github:web3-js/WebSocket-Node#ef5ea2f41daf4a2113b80c9223df884b4d56c400" }, "dependencies": { "websocket": { "version": "github:web3-js/WebSocket-Node#ef5ea2f41daf4a2113b80c9223df884b4d56c400", - "from": "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", @@ -8590,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": { @@ -11185,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": { diff --git a/package.json b/package.json index 45f62a3d..e9920b48 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/test/helpers/artifacts.js b/test/helpers/artifacts.js index 041c6442..2226128d 100644 --- a/test/helpers/artifacts.js +++ b/test/helpers/artifacts.js @@ -52,6 +52,7 @@ export const ExitNFT = artifacts.require('ExitNFT') // Misc export const GnosisSafeProxy = artifacts.require('GnosisSafeProxy') export const GnosisSafe = artifacts.require('GnosisSafe') +export const PolygonMigrationTest = artifacts.require('PolygonMigrationTest') // child chain export const childContracts = { diff --git a/test/integration/root/DepositManagerUpdate.test.js b/test/integration/root/DepositManagerUpdate.test.js new file mode 100644 index 00000000..d69583d1 --- /dev/null +++ b/test/integration/root/DepositManagerUpdate.test.js @@ -0,0 +1,152 @@ +import deployer from '../../helpers/deployer.js' +import * as utils from '../../helpers/utils.js' +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import * as artifacts from '../../helpers/artifacts.js' +import StatefulUtils from '../../helpers/StatefulUtils' + +const predicateTestUtils = require('./predicates/predicateTestUtils') +const ethUtils = require('ethereumjs-util') +const crypto = require('crypto') + +chai + .use(chaiAsPromised) + .should() + +contract('DepositManager Update @skip-on-coverage', async function(accounts) { + let depositManager, childContracts, registry, governance, e20, polygonMigrationTest, pol, statefulUtils, contracts + const amount = web3.utils.toBN('10').pow(web3.utils.toBN('18')) + + describe('test POL and MATIC behaviours', async function() { + before(async() => { + statefulUtils = new StatefulUtils() + }) + + beforeEach(async function() { + contracts = await deployer.freshDeploy(accounts[0]) + contracts.ERC20Predicate = await deployer.deployErc20Predicate() + depositManager = contracts.depositManager + registry = contracts.registry + governance = contracts.governance + childContracts = await deployer.initializeChildChain(accounts[0]) + + e20 = await deployer.deployMaticToken() + await governance.update( + registry.address, + registry.contract.methods.updateContractMap(ethUtils.keccak256('matic'), e20.rootERC20.address).encodeABI() + ) + + // deploy PolygonMigration test impl + polygonMigrationTest = await artifacts.PolygonMigrationTest.new() + + await governance.update( + registry.address, + registry.contract.methods.updateContractMap(ethUtils.keccak256('polygonMigration'), polygonMigrationTest.address).encodeABI() + ) + + pol = await artifacts.TestToken.new('Polygon Ecosystem Token', 'POL') + + await governance.update( + registry.address, + registry.contract.methods.updateContractMap(ethUtils.keccak256('pol'), pol.address).encodeABI() + ) + + await polygonMigrationTest.contract.methods.setTokenAddresses(e20.rootERC20.address, pol.address).send({ + from: accounts[0] + }) + + // mint POL to PolygonMigrationTest + await pol.contract.methods.mint(polygonMigrationTest.address, amount.toString()).send( + { from: accounts[0] } + ) + }) + + it('converts MATIC to POL using governance function', async() => { + // mint MATIC to depositManager + await e20.rootERC20.contract.methods.mint(depositManager.address, amount.toString()).send( + { from: accounts[0] } + ) + + // call migrateMatic using governance + await governance.update( + depositManager.address, + depositManager.contract.methods.migrateMatic(amount.toString()).encodeABI() + ) + + // check that MATIC balance has been converted to POL + const currentBalance = await pol.contract.methods.balanceOf(depositManager.address).call() + utils.assertBigNumberEquality(currentBalance, amount) + }) + + it('migrates to POL when depositing MATIC', async() => { + // deposit some MATIC + const bob = '0x' + crypto.randomBytes(20).toString('hex') + await utils.deposit( + depositManager, + childContracts.childChain, + e20.rootERC20, + bob, + amount, + { rootDeposit: true, erc20: true } + ) + + // check that MATIC balance has been converted to POL + const currentBalance = await pol.contract.methods.balanceOf(depositManager.address).call() + utils.assertBigNumberEquality(currentBalance, amount) + + // assert deposit on child chain + utils.assertBigNumberEquality(await e20.childToken.balanceOf(bob), amount) + }) + + it('bridges MATIC when depositing POL', async() => { + const bob = '0x' + crypto.randomBytes(20).toString('hex') + + // using the utils function more granularly here so we can call fireDepositFromMainToMatic with the correct token address + const depositBlockId = await utils.depositOnRoot( + depositManager, + pol, + bob, + amount, + { rootDeposit: true, erc20: true } + ) + await utils.fireDepositFromMainToMatic(childContracts.childChain, '0xa', bob, e20.rootERC20.address, amount, depositBlockId) + + // deposit on child chain is technically still in MATIC + utils.assertBigNumberEquality(await e20.childToken.balanceOf(bob), amount) + }) + + it('returns POL when withdrawing MATIC', async() => { + // no POL on this account + utils.assertBigNumberEquality(await pol.balanceOf(accounts[1]), 0) + + // deposit some MATIC + await utils.deposit( + depositManager, + childContracts.childChain, + e20.rootERC20, + accounts[1], + amount, + { rootDeposit: true, erc20: true } + ) + + // withdraw again + const { receipt } = await e20.childToken.withdraw(amount, { from: accounts[1], value: amount }) + + // submit checkpoint + let { block, blockProof, headerNumber, reference } = await statefulUtils.submitCheckpoint(contracts.rootChain, receipt, accounts) + + // call ERC20Predicate + await utils.startExitWithBurntTokens( + contracts.ERC20Predicate, + { headerNumber, blockProof, blockNumber: block.number, blockTimestamp: block.timestamp, reference, logIndex: 1 }, + accounts[1] + ) + + // process Exits for MATIC + await predicateTestUtils.processExits(contracts.withdrawManager, e20.rootERC20.address) + + // POL was received + utils.assertBigNumberEquality(await pol.balanceOf(accounts[1]), amount) + }) + }) +}) diff --git a/truffle-config.js b/truffle-config.js index 4f722cea..9196ba94 100644 --- a/truffle-config.js +++ b/truffle-config.js @@ -47,7 +47,7 @@ module.exports = { MNEMONIC, `https://rpc-mumbai.matic.today` ), - network_id: '80001', + network_id: '80001' }, goerli: { provider: function() {