diff --git a/.eslintrc.js b/.eslintrc.js index 26e60c8..8547190 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,8 +19,14 @@ module.exports = { "ethers": false, "upgrades": false, }, + plugins: [ + 'no-only-tests', + 'no-skip-tests', + ], rules: { - "max-len": ["error", 120, 2, { + 'no-skip-tests/no-skip-tests': 'warn', + 'no-only-tests/no-only-tests': 'warn', + 'max-len': ["error", 120, 2, { ignoreUrls: true, ignoreComments: false, ignoreRegExpLiterals: true, diff --git a/contracts/Erc20Vault.sol b/contracts/Erc20Vault.sol index af8ef90..4f6efeb 100644 --- a/contracts/Erc20Vault.sol +++ b/contracts/Erc20Vault.sol @@ -1,4 +1,9 @@ // SPDX-License-Identifier: MIT + +// NOTE: This special version of the pTokens-erc20-vault is for ETH mainnet, and includes custom +// logic to handle ETHPNT<->PNT fungibility, as well as custom logic to handle GALA tokens after +// they upgraded from v1 to v2. + pragma solidity ^0.8.0; import "./wEth/IWETH.sol"; @@ -30,6 +35,8 @@ contract Erc20Vault is IWETH public weth; bytes4 public ORIGIN_CHAIN_ID; address private wEthUnwrapperAddress; + address public constant PNT_TOKEN_ADDRESS = 0x89Ab32156e46F46D02ade3FEcbe5Fc4243B9AAeD; + address public constant ETHPNT_TOKEN_ADDRESS = 0xf4eA6B892853413bD9d9f1a5D3a620A0ba39c5b2; event PegIn( address _tokenAddress, @@ -146,8 +153,18 @@ contract Erc20Vault is { require(_tokenAmount > 0, "Token amount must be greater than zero!"); IERC20Upgradeable(_tokenAddress).safeTransferFrom(msg.sender, address(this), _tokenAmount); + + // NOTE: This is the special handling of the EthPNT token, where a peg in of EthPNT will + // result in an event which will mint a PNT pToken on the other side of the bridge, thus + // making fungible the PNT & EthPNT tokens. + address normalizedTokenAddress = _tokenAddress == ETHPNT_TOKEN_ADDRESS + ? PNT_TOKEN_ADDRESS + : _tokenAddress; + + require(normalizedTokenAddress != address(0), "`normalizedTokenAddress` is set to zero address!"); + emit PegIn( - _tokenAddress, + normalizedTokenAddress, msg.sender, _tokenAmount, _destinationAddress, @@ -155,6 +172,7 @@ contract Erc20Vault is ORIGIN_CHAIN_ID, _destinationChainId ); + return true; } @@ -263,14 +281,11 @@ contract Erc20Vault is ) public onlyPNetwork - returns (bool) + returns (bool success) { - if (_tokenAddress == address(weth)) { - pegOutWeth(_tokenRecipient, _tokenAmount, ""); - } else { - IERC20Upgradeable(_tokenAddress).safeTransfer(_tokenRecipient, _tokenAmount); - } - return true; + return _tokenAddress == address(weth) + ? pegOutWeth(_tokenRecipient, _tokenAmount, "") + : pegOutTokens(_tokenAddress, _tokenRecipient, _tokenAmount, ""); } function pegOut( @@ -283,17 +298,91 @@ contract Erc20Vault is onlyPNetwork returns (bool success) { - if (_tokenAddress == address(weth)) { - pegOutWeth(_tokenRecipient, _tokenAmount, _userData); + return _tokenAddress == address(weth) + ? pegOutWeth(_tokenRecipient, _tokenAmount, _userData) + : pegOutTokens(_tokenAddress, _tokenRecipient, _tokenAmount, _userData); + } + + function pegOutTokens( + address _tokenAddress, + address _tokenRecipient, + uint256 _tokenAmount, + bytes memory _userData + ) + internal + returns (bool success) + { + if (_tokenAddress == PNT_TOKEN_ADDRESS) { + return handlePntPegOut(_tokenRecipient, _tokenAmount, _userData); + } + + if (_tokenAddress == 0x15D4c048F83bd7e37d49eA4C83a07267Ec4203dA) { // NOTE: Gala v1 + return handleGalaV1PegOut(_tokenRecipient, _tokenAmount); + } + + if (tokenIsErc777(_tokenAddress)) { + // NOTE: This is an ERC777 token, so let's use its `send` function so that hooks are called... + IERC777Upgradeable(_tokenAddress).send(_tokenRecipient, _tokenAmount, _userData); } else { - address erc777Address = _erc1820.getInterfaceImplementer(_tokenAddress, Erc777Token_INTERFACE_HASH); - if (erc777Address == address(0)) { - return pegOut(_tokenRecipient, _tokenAddress, _tokenAmount); - } else { - IERC777Upgradeable(erc777Address).send(_tokenRecipient, _tokenAmount, _userData); - return true; - } + // NOTE: Otherwise, we use standard ERC20 transfer function instead. + IERC20Upgradeable(_tokenAddress).safeTransfer(_tokenRecipient, _tokenAmount); } + + return true; + } + + function tokenIsErc777(address _tokenAddress) view internal returns (bool) { + return _erc1820.getInterfaceImplementer(_tokenAddress, Erc777Token_INTERFACE_HASH) != address(0); + } + + function handleGalaV1PegOut( + address _tokenRecipient, + uint256 _tokenAmount + ) + internal + returns (bool success) + { + // NOTE: Neither Gala tokens implement hooks so we use a basic ERC20 transfer. + + IERC20Upgradeable(0x15D4c048F83bd7e37d49eA4C83a07267Ec4203dA) // NOTE Gala v1 + .safeTransfer(_tokenRecipient, _tokenAmount); + + IERC20Upgradeable(0xd1d2Eb1B1e90B638588728b4130137D262C87cae) // NOTE Gala v2 + .safeTransfer(_tokenRecipient, _tokenAmount); + + return true; + } + + function handlePntPegOut( + address _tokenRecipient, + uint256 _tokenAmount, + bytes memory _userData + ) + internal + returns (bool success) + { + // NOTE: The PNT contract is ERC777... + IERC777Upgradeable pntContract = IERC777Upgradeable(PNT_TOKEN_ADDRESS); + // NOTE: Whilst the EthPNT contract is ERC20. + IERC20Upgradeable ethPntContract = IERC20Upgradeable(ETHPNT_TOKEN_ADDRESS); + + // NOTE: First we need to know how much PNT this vault holds... + uint256 vaultPntTokenBalance = pntContract.balanceOf(address(this)); + + if (_tokenAmount <= vaultPntTokenBalance) { + // NOTE: If we can peg out _entirely_ with PNT tokens, we do so... + pntContract.send(_tokenRecipient, _tokenAmount, _userData); + } else if (vaultPntTokenBalance == 0) { + // NOTE: Here we must peg out entirely with ETHPNT tokens instead... + ethPntContract.safeTransfer(_tokenRecipient, _tokenAmount); + } else { + // NOTE: And so here we must peg out the total using as much PNT as possible, with + // the remainder being sent as EthPNT... + pntContract.send(_tokenRecipient, vaultPntTokenBalance, _userData); + ethPntContract.safeTransfer(_tokenRecipient, _tokenAmount - vaultPntTokenBalance); + } + + return true; } receive() external payable { } diff --git a/hardhat.config.js b/hardhat.config.js index 0916d2f..6707276 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -9,6 +9,7 @@ const { const { assoc } = require('ramda') require('hardhat-erc1820') +require('hardhat-storage-layout') require('@nomiclabs/hardhat-waffle') require('@nomiclabs/hardhat-etherscan') require('@openzeppelin/hardhat-upgrades') diff --git a/lib/show-existing-contract-addresses.js b/lib/show-existing-contract-addresses.js index 9008f4c..380bba9 100644 --- a/lib/show-existing-contract-addresses.js +++ b/lib/show-existing-contract-addresses.js @@ -9,7 +9,7 @@ const EXISTING_LOGIC_CONTRACT_ADDRESSES = [ { 'version': 'v2', 'chain': 'ropsten', 'address': '0x2Ea67a02058c3A0CE5f774949b6E8741B8D0a399' }, { 'version': 'v2', 'chain': 'rinkeby', 'address': '0x6819bbFdf803B8b87850916d3eEB3642DdE6C24F' }, { 'version': 'v2', 'chain': 'interim', 'address': '0xeEa7CE353a076898E35E82609e45918B5e4d0e0A' }, - { 'version': 'v2', 'chain': 'ethereum', 'address': '0xE01a9c36170b8Fa163C6a54D7aB3015C85e0186c' }, + { 'version': 'v2', 'chain': 'ethereum', 'address': '0xfbc347975C48578F4A25ECeEB61BC16356abE8a2' }, { 'version': 'v2', 'chain': 'goerli', 'address': '0xEa1FFBf0715FE7ccaae5d57dC698550f23581a27' }, { 'version': 'v2', 'chain': 'sepolia', 'address': '0x9fdbc63D5250Aa59cb6d5382eF4e92113fE1FC35' }, ] diff --git a/package-lock.json b/package-lock.json index 88f835d..712f0fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ptokens-erc20-vault-smart-contract", - "version": "2.7.0", + "version": "2.8.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ptokens-erc20-vault-smart-contract", - "version": "2.7.0", + "version": "2.8.0", "license": "MIT", "dependencies": { "@nomiclabs/hardhat-etherscan": "^3.1.0", @@ -31,10 +31,13 @@ "eslint-config-standard": "^12.0.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-mocha": "^5.3.0", + "eslint-plugin-no-only-tests": "^3.0.0", + "eslint-plugin-no-skip-tests": "^1.1.0", "eslint-plugin-node": "^8.0.1", "eslint-plugin-promise": "^4.3.1", "eslint-plugin-standard": "^4.1.0", "ethereum-waffle": "^3.4.4", + "hardhat-storage-layout": "^0.1.7", "mocha": "^6.2.3" } }, @@ -2751,6 +2754,15 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/console-table-printer": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.11.1.tgz", + "integrity": "sha512-8LfFpbF/BczoxPwo2oltto5bph8bJkGOATXsg3E9ddMJOGnWJciKHldx2zDj5XIBflaKzPfVCjOTl6tMh7lErg==", + "dev": true, + "dependencies": { + "simple-wcswidth": "^1.0.1" + } + }, "node_modules/cookie": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", @@ -3349,6 +3361,24 @@ "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==", "dev": true }, + "node_modules/eslint-plugin-no-only-tests": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.0.0.tgz", + "integrity": "sha512-I0PeXMs1vu21ap45hey4HQCJRqpcoIvGcNTPJe+UhUm8TwjQ6//mCrDqF8q0WS6LgmRDwQ4ovQej0AQsAHb5yg==", + "dev": true, + "engines": { + "node": ">=5.0.0" + } + }, + "node_modules/eslint-plugin-no-skip-tests": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-skip-tests/-/eslint-plugin-no-skip-tests-1.1.0.tgz", + "integrity": "sha512-o4Siv+8PqR8IilHnxdMogvZ2kAVpt3Zrz+LEQaWbremRn4cHcrrj+v+i4sr+ivQrsjz2NTxIRTTCEdYE90+ieQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eslint-plugin-node": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-8.0.1.tgz", @@ -12485,6 +12515,18 @@ "hardhat": "^2.0.0" } }, + "node_modules/hardhat-storage-layout": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/hardhat-storage-layout/-/hardhat-storage-layout-0.1.7.tgz", + "integrity": "sha512-q723g2iQnJpRdMC6Y8fbh/stG6MLHKNxa5jq/ohjtD5znOlOzQ6ojYuInY8V4o4WcPyG3ty4hzHYunLf66/1+A==", + "dev": true, + "dependencies": { + "console-table-printer": "^2.9.0" + }, + "peerDependencies": { + "hardhat": "^2.0.3" + } + }, "node_modules/hardhat/node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -16763,6 +16805,12 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/simple-wcswidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.0.1.tgz", + "integrity": "sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg==", + "dev": true + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -19923,6 +19971,15 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "console-table-printer": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.11.1.tgz", + "integrity": "sha512-8LfFpbF/BczoxPwo2oltto5bph8bJkGOATXsg3E9ddMJOGnWJciKHldx2zDj5XIBflaKzPfVCjOTl6tMh7lErg==", + "dev": true, + "requires": { + "simple-wcswidth": "^1.0.1" + } + }, "cookie": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", @@ -20410,6 +20467,18 @@ } } }, + "eslint-plugin-no-only-tests": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.0.0.tgz", + "integrity": "sha512-I0PeXMs1vu21ap45hey4HQCJRqpcoIvGcNTPJe+UhUm8TwjQ6//mCrDqF8q0WS6LgmRDwQ4ovQej0AQsAHb5yg==", + "dev": true + }, + "eslint-plugin-no-skip-tests": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-skip-tests/-/eslint-plugin-no-skip-tests-1.1.0.tgz", + "integrity": "sha512-o4Siv+8PqR8IilHnxdMogvZ2kAVpt3Zrz+LEQaWbremRn4cHcrrj+v+i4sr+ivQrsjz2NTxIRTTCEdYE90+ieQ==", + "dev": true + }, "eslint-plugin-node": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-8.0.1.tgz", @@ -27563,6 +27632,15 @@ "integrity": "sha512-oQxe7Li8Ev6/Gs6PMcH9+IjaXS+xh6HyPBTGnlRVG4yfmkYF7ajVvzxfYY/FGlM9/j+F2uZjRhxsc//qisC82A==", "requires": {} }, + "hardhat-storage-layout": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/hardhat-storage-layout/-/hardhat-storage-layout-0.1.7.tgz", + "integrity": "sha512-q723g2iQnJpRdMC6Y8fbh/stG6MLHKNxa5jq/ohjtD5znOlOzQ6ojYuInY8V4o4WcPyG3ty4hzHYunLf66/1+A==", + "dev": true, + "requires": { + "console-table-printer": "^2.9.0" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -30329,6 +30407,12 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "simple-wcswidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.0.1.tgz", + "integrity": "sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg==", + "dev": true + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/package.json b/package.json index 6e78c60..5108d95 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "ptokens-erc20-vault-smart-contract", - "version": "2.7.0", + "version": "2.8.0", "description": "The pToken ERC20 vault smart-contract & CLI", "main": "cli.js", "scripts": { + "lint": "npx eslint .", "test": "npm run tests", "tests": "ENDPOINT=http://localhost:8545 npx hardhat test", - "compile": "npx hardhat compile", - "lint": "npx eslint ." + "compile": "ENDPOINT=http://localhost:8545 npx hardhat compile", + "showStorage": "ENDPOINT=http://localhost:8545 node ./scripts/show-storage-layout.js" }, "repository": { "type": "git", @@ -45,10 +46,13 @@ "eslint-config-standard": "^12.0.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-mocha": "^5.3.0", + "eslint-plugin-no-only-tests": "^3.0.0", + "eslint-plugin-no-skip-tests": "^1.1.0", "eslint-plugin-node": "^8.0.1", "eslint-plugin-promise": "^4.3.1", "eslint-plugin-standard": "^4.1.0", "ethereum-waffle": "^3.4.4", + "hardhat-storage-layout": "^0.1.7", "mocha": "^6.2.3" } } diff --git a/scripts/show-storage-layout.js b/scripts/show-storage-layout.js new file mode 100644 index 0000000..ec84116 --- /dev/null +++ b/scripts/show-storage-layout.js @@ -0,0 +1,7 @@ +const hre = require("hardhat"); + +async function main() { + console.log(await hre.storageLayout.export()) +} + +main() diff --git a/test/pegging-in-ethpnt.test.js b/test/pegging-in-ethpnt.test.js new file mode 100644 index 0000000..a623ea4 --- /dev/null +++ b/test/pegging-in-ethpnt.test.js @@ -0,0 +1,125 @@ +const { + getRandomEthAddress, + deployUpgradeableContract, + deployNonUpgradeableContract, +} = require('./test-utils') +const { + ZERO_ADDRESS, + ADDRESS_PROP, +} = require('./test-constants') +const assert = require('assert') +const { prop } = require('ramda') +const { BigNumber } = require('ethers') + +// NOTE/FIXME This test is skipped because the PNT and EthPNT in the contract are hardcoded now. Initially +// they were editable by the pNetwork address, however this presented a possible attack surface. Instead, +// those addresses are now constants, (which in solidity do not take up a storage slot) since they're +// known ahead of time. However this now makes testing this behaviour difficult. One option is to do some +// trickery where we deploy two tokens, then re-write the contract (for tests only) on the fly, replacing +// the hardcoded constant addresses. +describe.skip('Pegging In EthPNT Tests', () => { + const SAMPLE_ORIGIN_CHAIN_ID = '0x00000000' + const VAULT_PATH = 'contracts/Erc20Vault.sol:Erc20Vault' + + let TOKEN_HOLDER, + VAULT_CONTRACT, + PNT_TOKEN_ADDRESS, + PNT_TOKEN_CONTRACT, + TOKEN_HOLDER_ADDRESS, + ETHPNT_TOKEN_ADDRESS, + ETHPNT_TOKEN_CONTRACT, + VAULT_CONTRACT_ADDRESS + + beforeEach(async () => { + const TOKEN_HOLDER_BALANCE = 1e6 + const signers = await ethers.getSigners() + const WETH_ADDRESS = getRandomEthAddress() + + // NOTE: Create a token holder for us to play with.. + TOKEN_HOLDER = signers[1] + TOKEN_HOLDER_ADDRESS = prop(ADDRESS_PROP, TOKEN_HOLDER) + + // NOTE: Deploy the vault contract itself... + VAULT_CONTRACT = await deployUpgradeableContract(VAULT_PATH, [ WETH_ADDRESS, [], SAMPLE_ORIGIN_CHAIN_ID ]) + VAULT_CONTRACT_ADDRESS = prop(ADDRESS_PROP, VAULT_CONTRACT) + + // Deploy two contracts to mock the PNT and ETHPNT tokens... + PNT_TOKEN_CONTRACT = await deployNonUpgradeableContract('contracts/test-contracts/Erc20Token.sol:Erc20Token') + ETHPNT_TOKEN_CONTRACT = await deployNonUpgradeableContract('contracts/test-contracts/Erc20Token.sol:Erc20Token') + PNT_TOKEN_ADDRESS = prop(ADDRESS_PROP, PNT_TOKEN_CONTRACT) + ETHPNT_TOKEN_ADDRESS = prop(ADDRESS_PROP, ETHPNT_TOKEN_CONTRACT) + + // NOTE: Supply our token holder with some ETHPNT to peg in... + await ETHPNT_TOKEN_CONTRACT.transfer(TOKEN_HOLDER_ADDRESS, TOKEN_HOLDER_BALANCE) + + // NOTE: Assert that the holder actually has those tokens... + let tokenHolderEthPntBalance = await ETHPNT_TOKEN_CONTRACT.balanceOf(TOKEN_HOLDER_ADDRESS) + assert(tokenHolderEthPntBalance.eq(BigNumber.from(TOKEN_HOLDER_BALANCE))) + + // NOTE: Add the EthPNT token as a supported token in the vault... + await VAULT_CONTRACT.addSupportedToken(ETHPNT_TOKEN_ADDRESS) + assert(await VAULT_CONTRACT.isTokenSupported(ETHPNT_TOKEN_ADDRESS)) + + // NOTE approve the vault to spend the EthPNT tokens the token holder holds, so they can be pegged in... + await ETHPNT_TOKEN_CONTRACT.connect(TOKEN_HOLDER).approve(VAULT_CONTRACT_ADDRESS, TOKEN_HOLDER_BALANCE) + + // NOTE: Set the PNT & EthPNT token addresses correctly in the vault... + await VAULT_CONTRACT.changePntTokenAddress(PNT_TOKEN_ADDRESS) + await VAULT_CONTRACT.changeEthPntTokenAddress(ETHPNT_TOKEN_ADDRESS) + assert.strictEqual(await VAULT_CONTRACT.PNT_TOKEN_ADDRESS(), PNT_TOKEN_ADDRESS) + assert.strictEqual(await VAULT_CONTRACT.ETHPNT_TOKEN_ADDRESS(), ETHPNT_TOKEN_ADDRESS) + }) + + it('Pegging in EthPNT token should fire event with token address as PNT token', async () => { + const tokenAmount = 1337 + const destinationAddress = getRandomEthAddress() + const userData = '0xc0ffee' + const destinationChainId = '0xffffffff' + + // NOTE: We have to call the fxn this way because its overloaded in the contract... + const tx = await VAULT_CONTRACT.connect(TOKEN_HOLDER)['pegIn(uint256,address,string,bytes,bytes4)']( + tokenAmount, + ETHPNT_TOKEN_ADDRESS, + destinationAddress, + userData, + destinationChainId, + ) + const txReceipt = await tx.wait() + const expectedNumEvents = 3 + assert.strictEqual(txReceipt.events.length, expectedNumEvents) + const pegInEvent = txReceipt.events[expectedNumEvents - 1] + + // NOTE: Assert the event args... + assert.strictEqual(pegInEvent.args[0], PNT_TOKEN_ADDRESS) // NOTE: This is the one we care about here! + assert.strictEqual(pegInEvent.args[1], TOKEN_HOLDER_ADDRESS) + assert(pegInEvent.args[2].eq(tokenAmount)) + assert.strictEqual(pegInEvent.args[3], destinationAddress) + assert.strictEqual(pegInEvent.args[4], userData) + assert.strictEqual(pegInEvent.args[5], SAMPLE_ORIGIN_CHAIN_ID) + assert.strictEqual(pegInEvent.args[6], destinationChainId) + }) + + it('Should fail to peg in if `PNT_TOKEN_ADDRESS` is set to zero', async () => { + await VAULT_CONTRACT.changePntTokenAddress(ZERO_ADDRESS) + assert.strictEqual(await VAULT_CONTRACT.PNT_TOKEN_ADDRESS(), ZERO_ADDRESS) + + const tokenAmount = 1337 + const destinationAddress = getRandomEthAddress() + const userData = '0xc0ffee' + const destinationChainId = '0xffffffff' + + try { + await VAULT_CONTRACT.connect(TOKEN_HOLDER)['pegIn(uint256,address,string,bytes,bytes4)']( + tokenAmount, + ETHPNT_TOKEN_ADDRESS, + destinationAddress, + userData, + destinationChainId, + ) + assert.fail('Should not have succeeded!') + } catch (_err) { + const expectedError = '`PNT_TOKEN_ADDRESS` is set to zero address!' + assert(_err.message.includes(expectedError)) + } + }) +}) diff --git a/test/pegging-out-pnt.test.js b/test/pegging-out-pnt.test.js new file mode 100644 index 0000000..12c131c --- /dev/null +++ b/test/pegging-out-pnt.test.js @@ -0,0 +1,178 @@ +const { + getRandomEthAddress, + deployUpgradeableContract, + deployNonUpgradeableContract, +} = require('./test-utils') +const assert = require('assert') +const { prop } = require('ramda') +const { ADDRESS_PROP } = require('./test-constants') + +// NOTE/FIXME This test is skipped because the PNT and EthPNT in the contract are hardcoded now. Initially +// they were editable by the pNetwork address, however this presented a possible attack surface. Instead, +// those addresses are now constants, (which in solidity do not take up a storage slot) since they're +// known ahead of time. However this now makes testing this behaviour difficult. One option is to do some +// trickery where we deploy two tokens, then re-write the contract (for tests only) on the fly, replacing +// the hardcoded constant addresses. +describe.skip('Pegging Out PNT Tests', () => { + const VAULT_TOKEN_APPROVAL_AMOUNT = 1e6 + const SAMPLE_ORIGIN_CHAIN_ID = '0x00000000' + const VAULT_PATH = 'contracts/Erc20Vault.sol:Erc20Vault' + + let TOKEN_HOLDER, + VAULT_CONTRACT, + PNT_TOKEN_ADDRESS, + PNT_TOKEN_CONTRACT, + TOKEN_HOLDER_ADDRESS, + ETHPNT_TOKEN_ADDRESS, + ETHPNT_TOKEN_CONTRACT, + VAULT_CONTRACT_ADDRESS + + beforeEach(async () => { + const signers = await ethers.getSigners() + const WETH_ADDRESS = getRandomEthAddress() + + // NOTE: Create a token holder for us to play with.. + TOKEN_HOLDER = signers[1] + TOKEN_HOLDER_ADDRESS = prop(ADDRESS_PROP, TOKEN_HOLDER) + + // NOTE: Deploy the vault contract itself... + VAULT_CONTRACT = await deployUpgradeableContract(VAULT_PATH, [ WETH_ADDRESS, [], SAMPLE_ORIGIN_CHAIN_ID ]) + VAULT_CONTRACT_ADDRESS = prop(ADDRESS_PROP, VAULT_CONTRACT) + + // Deploy two contracts to mock the PNT and ETHPNT tokens... + PNT_TOKEN_CONTRACT = await deployNonUpgradeableContract('contracts/test-contracts/Erc20Token.sol:Erc20Token') + ETHPNT_TOKEN_CONTRACT = await deployNonUpgradeableContract('contracts/test-contracts/Erc20Token.sol:Erc20Token') + PNT_TOKEN_ADDRESS = prop(ADDRESS_PROP, PNT_TOKEN_CONTRACT) + ETHPNT_TOKEN_ADDRESS = prop(ADDRESS_PROP, ETHPNT_TOKEN_CONTRACT) + + // NOTE: Add the EthPNT token as a supported token in the vault... + await VAULT_CONTRACT.addSupportedToken(ETHPNT_TOKEN_ADDRESS) + assert(await VAULT_CONTRACT.isTokenSupported(ETHPNT_TOKEN_ADDRESS)) + + // NOTE approve the vault to spend the EthPNT tokens the token holder holds, so they can be pegged in... + await ETHPNT_TOKEN_CONTRACT + .connect(TOKEN_HOLDER) + .approve(VAULT_CONTRACT_ADDRESS, VAULT_TOKEN_APPROVAL_AMOUNT) + + // NOTE: Set the PNT & EthPNT token addresses correctly in the vault... + await VAULT_CONTRACT.changePntTokenAddress(PNT_TOKEN_ADDRESS) + await VAULT_CONTRACT.changeEthPntTokenAddress(ETHPNT_TOKEN_ADDRESS) + assert.strictEqual(await VAULT_CONTRACT.PNT_TOKEN_ADDRESS(), PNT_TOKEN_ADDRESS) + assert.strictEqual(await VAULT_CONTRACT.ETHPNT_TOKEN_ADDRESS(), ETHPNT_TOKEN_ADDRESS) + }) + + it('Should peg out PNT entirely in PNT if vault PNT balance is sufficient', async () => { + const pegOutAmount = 1337 + + // NOTE: Give the vault sufficient balance of the PNT token for our pegout.... + PNT_TOKEN_CONTRACT.transfer(VAULT_CONTRACT_ADDRESS, pegOutAmount * 2) + + // NOTE: Assert the vault's balances before... + const vaultPntBalanceBefore = await PNT_TOKEN_CONTRACT.balanceOf(VAULT_CONTRACT_ADDRESS) + const vaultEthPntBalanceBefore = await ETHPNT_TOKEN_CONTRACT.balanceOf(VAULT_CONTRACT_ADDRESS) + assert(vaultPntBalanceBefore.eq(pegOutAmount * 2)) + assert(vaultEthPntBalanceBefore.eq(0)) + + // NOTE: Assert the token holder's balances before... + const tokenHolderPntBalanceBefore = await PNT_TOKEN_CONTRACT.balanceOf(TOKEN_HOLDER_ADDRESS) + const tokenHolderEthPntBalanceBefore = await ETHPNT_TOKEN_CONTRACT.balanceOf(TOKEN_HOLDER_ADDRESS) + assert(tokenHolderPntBalanceBefore.eq(0)) + assert(tokenHolderEthPntBalanceBefore.eq(0)) + + // NOTE: We have to call the `pegOut` fxn this way because its overloaded in the contract... + await VAULT_CONTRACT['pegOut(address,address,uint256)']( + TOKEN_HOLDER_ADDRESS, + PNT_TOKEN_ADDRESS, + pegOutAmount, + ) + + // Assert the vault balances have changed as expected... + const vaultPntBalanceAfter = await PNT_TOKEN_CONTRACT.balanceOf(VAULT_CONTRACT_ADDRESS) + const vaultEthPntBalanceAfter = await ETHPNT_TOKEN_CONTRACT.balanceOf(VAULT_CONTRACT_ADDRESS) + assert(vaultPntBalanceAfter.eq(vaultPntBalanceBefore - pegOutAmount)) + assert(vaultEthPntBalanceAfter.eq(0)) + + // NOTE: Assert the token holder's balances have changed as expected + const tokenHolderPntBalanceAfter = await PNT_TOKEN_CONTRACT.balanceOf(TOKEN_HOLDER_ADDRESS) + const tokenHolderEthPntBalanceAfter = await ETHPNT_TOKEN_CONTRACT.balanceOf(TOKEN_HOLDER_ADDRESS) + assert(tokenHolderPntBalanceAfter.eq(pegOutAmount)) + assert(tokenHolderEthPntBalanceAfter.eq(0)) + }) + + it('Should peg out PNT entirely in EthPNT if vault PNT balance is zero', async () => { + const pegOutAmount = 1337 + + // NOTE: Give the vault sufficient balance of the EthPNT token for our pegout.... + ETHPNT_TOKEN_CONTRACT.transfer(VAULT_CONTRACT_ADDRESS, pegOutAmount * 2) + + // NOTE: Assert the vault's balances before... + const vaultPntBalanceBefore = await PNT_TOKEN_CONTRACT.balanceOf(VAULT_CONTRACT_ADDRESS) + const vaultEthPntBalanceBefore = await ETHPNT_TOKEN_CONTRACT.balanceOf(VAULT_CONTRACT_ADDRESS) + assert(vaultPntBalanceBefore.eq(0)) + assert(vaultEthPntBalanceBefore.eq(pegOutAmount * 2)) + + // NOTE: Assert the token holder's balances before... + const tokenHolderPntBalanceBefore = await PNT_TOKEN_CONTRACT.balanceOf(TOKEN_HOLDER_ADDRESS) + const tokenHolderEthPntBalanceBefore = await ETHPNT_TOKEN_CONTRACT.balanceOf(TOKEN_HOLDER_ADDRESS) + assert(tokenHolderPntBalanceBefore.eq(0)) + assert(tokenHolderEthPntBalanceBefore.eq(0)) + + // NOTE: We have to call the `pegOut` fxn this way because its overloaded in the contract... + await VAULT_CONTRACT['pegOut(address,address,uint256)']( + TOKEN_HOLDER_ADDRESS, + PNT_TOKEN_ADDRESS, + pegOutAmount, + ) + + // Assert the vault balances have changed as expected... + const vaultPntBalanceAfter = await PNT_TOKEN_CONTRACT.balanceOf(VAULT_CONTRACT_ADDRESS) + const vaultEthPntBalanceAfter = await ETHPNT_TOKEN_CONTRACT.balanceOf(VAULT_CONTRACT_ADDRESS) + assert(vaultPntBalanceAfter.eq(0)) + assert(vaultEthPntBalanceAfter.eq(vaultEthPntBalanceBefore - pegOutAmount)) + + // NOTE: Assert the token holder's balances have changed as expected + const tokenHolderPntBalanceAfter = await PNT_TOKEN_CONTRACT.balanceOf(TOKEN_HOLDER_ADDRESS) + const tokenHolderEthPntBalanceAfter = await ETHPNT_TOKEN_CONTRACT.balanceOf(TOKEN_HOLDER_ADDRESS) + assert(tokenHolderPntBalanceAfter.eq(0)) + assert(tokenHolderEthPntBalanceAfter.eq(pegOutAmount)) + }) + + it('Should peg out PNT in both PNT & EthPNT if vault PNT balance is not sufficient but not zero', async () => { + const pegOutAmount = 1337 + const insufficientPntAmount = Math.floor(pegOutAmount / 2) + const expectedEthPntAmount = pegOutAmount - insufficientPntAmount + + // NOTE: Give the vault insufficient balance of the PNT token for our pegout.... + PNT_TOKEN_CONTRACT.transfer(VAULT_CONTRACT_ADDRESS, insufficientPntAmount) + + // NOTE: Give the vault some balance of the EthPNT token to complete our pegout.... + ETHPNT_TOKEN_CONTRACT.transfer(VAULT_CONTRACT_ADDRESS, pegOutAmount * 2) + + // NOTE: Assert the vault's balances before... + const vaultPntBalanceBefore = await PNT_TOKEN_CONTRACT.balanceOf(VAULT_CONTRACT_ADDRESS) + const vaultEthPntBalanceBefore = await ETHPNT_TOKEN_CONTRACT.balanceOf(VAULT_CONTRACT_ADDRESS) + assert(vaultPntBalanceBefore.eq(insufficientPntAmount)) + assert(vaultEthPntBalanceBefore.eq(pegOutAmount * 2)) + + // NOTE: Assert the token holder's balances before... + const tokenHolderPntBalanceBefore = await PNT_TOKEN_CONTRACT.balanceOf(TOKEN_HOLDER_ADDRESS) + const tokenHolderEthPntBalanceBefore = await ETHPNT_TOKEN_CONTRACT.balanceOf(TOKEN_HOLDER_ADDRESS) + assert(tokenHolderPntBalanceBefore.eq(0)) + assert(tokenHolderEthPntBalanceBefore.eq(0)) + + // NOTE: We have to call the `pegOut` fxn this way because its overloaded in the contract... + await VAULT_CONTRACT['pegOut(address,address,uint256)'](TOKEN_HOLDER_ADDRESS, PNT_TOKEN_ADDRESS, pegOutAmount) + + // Assert the vault balances have changed as expected... + const vaultPntBalanceAfter = await PNT_TOKEN_CONTRACT.balanceOf(VAULT_CONTRACT_ADDRESS) + const vaultEthPntBalanceAfter = await ETHPNT_TOKEN_CONTRACT.balanceOf(VAULT_CONTRACT_ADDRESS) + assert(vaultPntBalanceAfter.eq(0)) + assert(vaultEthPntBalanceAfter.eq(vaultEthPntBalanceBefore - expectedEthPntAmount)) + + // NOTE: Assert the token holder's balances have changed as expected + const tokenHolderPntBalanceAfter = await PNT_TOKEN_CONTRACT.balanceOf(TOKEN_HOLDER_ADDRESS) + const tokenHolderEthPntBalanceAfter = await ETHPNT_TOKEN_CONTRACT.balanceOf(TOKEN_HOLDER_ADDRESS) + assert(tokenHolderPntBalanceAfter.eq(insufficientPntAmount)) + assert(tokenHolderEthPntBalanceAfter.eq(expectedEthPntAmount)) + }) +}) diff --git a/test/test-constants.js b/test/test-constants.js index f6cee54..108320b 100644 --- a/test/test-constants.js +++ b/test/test-constants.js @@ -3,4 +3,5 @@ module.exports = { ADDRESS_PROP: 'address', ORIGIN_CHAIN_ID: '0x0069c322', DESTINATION_CHAIN_ID: '0x00f34368', + ZERO_ADDRESS: '0x0000000000000000000000000000000000000000', }