diff --git a/.vscode/settings.json b/.vscode/settings.json index 6720ec14e..570ae0585 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,5 +25,6 @@ }, "[typescript]": { "editor.formatOnSave": true - } + }, + "solidity.compileUsingRemoteVersion": "v0.6.12+commit.27d51765" } diff --git a/contracts/test/UpgradedFiatTokenNewFieldsV2_2Test.sol b/contracts/test/UpgradedFiatTokenNewFieldsV2_2Test.sol new file mode 100644 index 000000000..20937a69d --- /dev/null +++ b/contracts/test/UpgradedFiatTokenNewFieldsV2_2Test.sol @@ -0,0 +1,74 @@ +/** + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.6.12; + +import { FiatTokenV2_2 } from "../v2/FiatTokenV2_2.sol"; + +/** + * @title UpgradedFiatTokenNewFieldsTest + * @dev ERC20 Token backed by fiat reserves + */ +contract UpgradedFiatTokenNewFieldsV2_2Test is FiatTokenV2_2 { + bool public newBool; + address public newAddress; + uint256 public newUint; + bool internal initializedV2; + + function initialize( + string calldata tokenName, + string calldata tokenSymbol, + string calldata tokenCurrency, + uint8 tokenDecimals, + address newMasterMinter, + address newPauser, + address newBlacklister, + address newOwner, + bool _newBool, + address _newAddress, + uint256 _newUint + ) external { + super.initialize( + tokenName, + tokenSymbol, + tokenCurrency, + tokenDecimals, + newMasterMinter, + newPauser, + newBlacklister, + newOwner + ); + initV2(_newBool, _newAddress, _newUint); + } + + function initV2( + bool _newBool, + address _newAddress, + uint256 _newUint + ) public { + require(!initializedV2, "contract is already initialized"); + newBool = _newBool; + newAddress = _newAddress; + newUint = _newUint; + initializedV2 = true; + } +} diff --git a/contracts/test/UpgradedFiatTokenV2_2.sol b/contracts/test/UpgradedFiatTokenV2_2.sol new file mode 100644 index 000000000..65daa22b7 --- /dev/null +++ b/contracts/test/UpgradedFiatTokenV2_2.sol @@ -0,0 +1,33 @@ +/** + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.6.12; + +import { FiatTokenV2_2 } from "../v2/FiatTokenV2_2.sol"; + +/** + * @title UpgradedFiatToken + * @dev ERC20 Token backed by fiat reserves + */ +contract UpgradedFiatTokenV2_2 is FiatTokenV2_2 { + +} diff --git a/contracts/v1/Blacklistable.sol b/contracts/v1/Blacklistable.sol index c1827e836..732c63614 100644 --- a/contracts/v1/Blacklistable.sol +++ b/contracts/v1/Blacklistable.sol @@ -53,7 +53,7 @@ contract Blacklistable is Ownable { * @dev Throws if argument account is blacklisted * @param _account The address to check */ - modifier notBlacklisted(address _account) { + modifier notBlacklisted(address _account) virtual { require( !blacklisted[_account], "Blacklistable: account is blacklisted" @@ -65,7 +65,7 @@ contract Blacklistable is Ownable { * @dev Checks if account is blacklisted * @param _account The address to check */ - function isBlacklisted(address _account) external view returns (bool) { + function isBlacklisted(address _account) virtual external view returns (bool) { return blacklisted[_account]; } @@ -73,7 +73,7 @@ contract Blacklistable is Ownable { * @dev Adds account to blacklist * @param _account The address to blacklist */ - function blacklist(address _account) external onlyBlacklister { + function blacklist(address _account) virtual external onlyBlacklister { blacklisted[_account] = true; emit Blacklisted(_account); } @@ -82,7 +82,7 @@ contract Blacklistable is Ownable { * @dev Removes account from blacklist * @param _account The address to remove from the blacklist */ - function unBlacklist(address _account) external onlyBlacklister { + function unBlacklist(address _account) virtual external onlyBlacklister { blacklisted[_account] = false; emit UnBlacklisted(_account); } diff --git a/contracts/v1/FiatTokenV1.sol b/contracts/v1/FiatTokenV1.sol index 1820842d1..4f3d7b453 100644 --- a/contracts/v1/FiatTokenV1.sol +++ b/contracts/v1/FiatTokenV1.sol @@ -111,6 +111,7 @@ contract FiatTokenV1 is AbstractFiatTokenV1, Ownable, Pausable, Blacklistable { * @return A boolean that indicates if the operation was successful. */ function mint(address _to, uint256 _amount) + virtual external whenNotPaused onlyMinters @@ -190,6 +191,7 @@ contract FiatTokenV1 is AbstractFiatTokenV1, Ownable, Pausable, Blacklistable { * @param account address The account */ function balanceOf(address account) + virtual external override view @@ -246,6 +248,7 @@ contract FiatTokenV1 is AbstractFiatTokenV1, Ownable, Pausable, Blacklistable { address to, uint256 value ) + virtual external override whenNotPaused @@ -270,6 +273,7 @@ contract FiatTokenV1 is AbstractFiatTokenV1, Ownable, Pausable, Blacklistable { * @return True if successful */ function transfer(address to, uint256 value) + virtual external override whenNotPaused @@ -291,7 +295,7 @@ contract FiatTokenV1 is AbstractFiatTokenV1, Ownable, Pausable, Blacklistable { address from, address to, uint256 value - ) internal override { + ) virtual internal override { require(from != address(0), "ERC20: transfer from the zero address"); require(to != address(0), "ERC20: transfer to the zero address"); require( diff --git a/contracts/v2/FiatTokenV2.sol b/contracts/v2/FiatTokenV2.sol index 5535df3ba..553052f02 100644 --- a/contracts/v2/FiatTokenV2.sol +++ b/contracts/v2/FiatTokenV2.sol @@ -106,7 +106,7 @@ contract FiatTokenV2 is FiatTokenV1_1, EIP3009, EIP2612 { uint8 v, bytes32 r, bytes32 s - ) external whenNotPaused notBlacklisted(from) notBlacklisted(to) { + ) virtual external whenNotPaused notBlacklisted(from) notBlacklisted(to) { _transferWithAuthorization( from, to, @@ -144,7 +144,7 @@ contract FiatTokenV2 is FiatTokenV1_1, EIP3009, EIP2612 { uint8 v, bytes32 r, bytes32 s - ) external whenNotPaused notBlacklisted(from) notBlacklisted(to) { + ) virtual external whenNotPaused notBlacklisted(from) notBlacklisted(to) { _receiveWithAuthorization( from, to, diff --git a/contracts/v2/FiatTokenV2_1.sol b/contracts/v2/FiatTokenV2_1.sol index d7eaf65ef..11a7b56c8 100644 --- a/contracts/v2/FiatTokenV2_1.sol +++ b/contracts/v2/FiatTokenV2_1.sol @@ -54,7 +54,7 @@ contract FiatTokenV2_1 is FiatTokenV2 { * @notice Version string for the EIP712 domain separator * @return Version string */ - function version() external view returns (string memory) { + function version() virtual external view returns (string memory) { return "2"; } } diff --git a/contracts/v2/FiatTokenV2_2.sol b/contracts/v2/FiatTokenV2_2.sol new file mode 100644 index 000000000..f5ce08653 --- /dev/null +++ b/contracts/v2/FiatTokenV2_2.sol @@ -0,0 +1,330 @@ +/** + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.6.12; + +import { FiatTokenV2_1 } from "../v2/FiatTokenV2_1.sol"; +import { EIP712 } from "../util/EIP712.sol"; + +// solhint-disable func-name-mixedcase + +/** + * @title FiatTokenV2_2 + * @dev ERC20 Token backed by fiat reserves + */ +contract FiatTokenV2_2 is FiatTokenV2_1 { + /** + * @notice Initialize v2.2 + */ + function initializeV2_2(address[] calldata accountsToBlacklist) external { + require(_initializedVersion == 2, "v2.2 initialized out of order"); + + // re-add previously blacklisted accounts to the blacklist + // by setting the high bit of their balance to 1 + for (uint256 i = 0; i < accountsToBlacklist.length; i++) { + _blacklist(accountsToBlacklist[i]); + } + + // additionally blacklist the contract address itself + _blacklist(address(this)); + + _initializedVersion = 3; + } + + /** + * @dev Adds account to blacklist + * @param _account The address to blacklist + */ + function blacklist(address _account) override external onlyBlacklister { + _blacklist(_account); + } + + /** + * @dev Internal function to process additions to the blacklist + * @param _account The address to blacklist + */ + function _blacklist(address _account) internal { + balances[_account] = balances[_account] | (uint256(1) << 255); + emit Blacklisted(_account); + } + + /** + * @dev Removes account from blacklist + * @param _account The address to remove from the blacklist + */ + function unBlacklist(address _account) override external onlyBlacklister { + _unBlacklist(_account); + } + + /** + * @dev Internal function to process removals from the blacklist + * @param _account The address to remove from the blacklist + */ + function _unBlacklist(address _account) internal { + balances[_account] = balances[_account] & ~(uint256(1) << 255); + emit UnBlacklisted(_account); + } + + /** + * @dev Checks if account is blacklisted + * @param _account The address to check + */ + function isBlacklisted(address _account) override external view returns (bool) { + return balances[_account] >> 255 == 1; + } + + /** + * @dev Throws if argument account is blacklisted + * @param _account The address to check + */ + modifier notBlacklisted(address _account) override { + require( + !(balances[_account] >> 255 == 1), + "Blacklistable: account is blacklisted" + ); + _; + } + + /** + * @dev Get token balance of an account + * @param account address The account + */ + function balanceOf(address account) + external + override + view + returns (uint256) + { + // return balance without effect of blacklist high bit + return balances[account] & ~(uint256(1) << 255); + } + + /** + * @dev Get token balance of an account + * @param account address The account + */ + function _getBalanceAndBlacklistStatus(address account) + internal + view + returns (uint256, bool) + { + uint256 balance = balances[account]; + return (balance & ~(uint256(1) << 255), balance >> 255 == 1); + } + + /** + * @notice Internal function to process transfers + * @param from Payer's address + * @param to Payee's address + * @param value Transfer amount + */ + function _transfer( + address from, + address to, + uint256 value + ) internal override { + require(from != address(0), "ERC20: transfer from the zero address"); + require(to != address(0), "ERC20: transfer to the zero address"); + + (uint256 fromBalance, bool isFromBlacklisted) = _getBalanceAndBlacklistStatus(from); + + require(!isFromBlacklisted, "Blacklistable: account is blacklisted"); + + require( + value <= fromBalance, + "ERC20: transfer amount exceeds balance" + ); + + balances[from] = fromBalance.sub(value); + + (uint256 toBalance, bool isToBlacklisted) = _getBalanceAndBlacklistStatus(to); + require(!isToBlacklisted, "Blacklistable: account is blacklisted"); + + balances[to] = toBalance.add(value); + + emit Transfer(from, to, value); + } + + /** + * @notice Transfer tokens from the caller + * @param to Payee's address + * @param value Transfer amount + * @return True if successful + */ + function transfer(address to, uint256 value) + external + override + whenNotPaused + returns (bool) + { + _transfer(msg.sender, to, value); + return true; + } + + /** + * @notice Transfer tokens by spending allowance + * @param from Payer's address + * @param to Payee's address + * @param value Transfer amount + * @return True if successful + */ + function transferFrom( + address from, + address to, + uint256 value + ) + virtual + external + override + whenNotPaused + notBlacklisted(msg.sender) + returns (bool) + { + require( + value <= allowed[from][msg.sender], + "ERC20: transfer amount exceeds allowance" + ); + _transfer(from, to, value); + + // allow for infinite allowance gas saving trick + if (allowed[from][msg.sender] != uint256(-1)) { + allowed[from][msg.sender] = allowed[from][msg.sender].sub(value); + } + + return true; + } + + /** + * @notice Execute a transfer with a signed authorization + * @param from Payer's address (Authorizer) + * @param to Payee's address + * @param value Amount to be transferred + * @param validAfter The time after which this is valid (unix time) + * @param validBefore The time before which this is valid (unix time) + * @param nonce Unique nonce + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function transferWithAuthorization( + address from, + address to, + uint256 value, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) override external whenNotPaused { + _transferWithAuthorization( + from, + to, + value, + validAfter, + validBefore, + nonce, + v, + r, + s + ); + } + + /** + * @notice Receive a transfer with a signed authorization from the payer + * @dev This has an additional check to ensure that the payee's address + * matches the caller of this function to prevent front-running attacks. + * @param from Payer's address (Authorizer) + * @param to Payee's address + * @param value Amount to be transferred + * @param validAfter The time after which this is valid (unix time) + * @param validBefore The time before which this is valid (unix time) + * @param nonce Unique nonce + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function receiveWithAuthorization( + address from, + address to, + uint256 value, + uint256 validAfter, + uint256 validBefore, + bytes32 nonce, + uint8 v, + bytes32 r, + bytes32 s + ) override external whenNotPaused { + _receiveWithAuthorization( + from, + to, + value, + validAfter, + validBefore, + nonce, + v, + r, + s + ); + } + + /** + * @dev Function to mint tokens + * @param _to The address that will receive the minted tokens. + * @param _amount The amount of tokens to mint. Must be less than or equal + * to the minterAllowance of the caller. + * @return A boolean that indicates if the operation was successful. + */ + function mint(address _to, uint256 _amount) + override + external + whenNotPaused + onlyMinters + notBlacklisted(msg.sender) + notBlacklisted(_to) + returns (bool) + { + require(_to != address(0), "FiatToken: mint to the zero address"); + require(_amount > 0, "FiatToken: mint amount not greater than 0"); + + uint256 mintingAllowedAmount = minterAllowed[msg.sender]; + require( + _amount <= mintingAllowedAmount, + "FiatToken: mint amount exceeds minterAllowance" + ); + + uint256 newTotalSupply = totalSupply_.add(_amount); + // The supply cap ensures that no account can be unintentionally blacklisted + // with a high balance + // Hardcoding the value here as opposed to setting it in storage since it's + // expected to be static and this avoids an SLOAD + require(newTotalSupply <= (uint256(1) << 255) - 1, "mint causes total supply to exceed supply cap"); + + totalSupply_ = newTotalSupply; + + balances[_to] = balances[_to].add(_amount); + minterAllowed[msg.sender] = mintingAllowedAmount.sub(_amount); + emit Mint(msg.sender, _to, _amount); + emit Transfer(address(0), _to, _amount); + return true; + } +} \ No newline at end of file diff --git a/contracts/v2/upgrader/V2_2Upgrader.sol b/contracts/v2/upgrader/V2_2Upgrader.sol new file mode 100644 index 000000000..b29e2fe68 --- /dev/null +++ b/contracts/v2/upgrader/V2_2Upgrader.sol @@ -0,0 +1,221 @@ +/** + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pragma solidity 0.6.12; + +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Ownable } from "../../v1/Ownable.sol"; +import { FiatTokenV2_2 } from "../FiatTokenV2_2.sol"; +import { FiatTokenProxy } from "../../v1/FiatTokenProxy.sol"; +import { V2UpgraderHelper } from "../../v2/upgrader/V2UpgraderHelper.sol"; + +/** + * @title v2.2 Upgrader + * @notice Performs USDC v2.2 upgrade + * @dev docs TBD + */ +contract V2_2Upgrader is Ownable { + using SafeMath for uint256; + + FiatTokenProxy private _proxy; + FiatTokenV2_2 private _implementation; + address private _newProxyAdmin; + string private _newName; + V2UpgraderHelper private _helper; + address[] private _accountsToBlacklist; + + /** + * @notice Constructor + * @param proxy FiatTokenProxy contract + * @param implementation FiatTokenV2_2 implementation contract + * @param newProxyAdmin Grantee of proxy admin role after upgrade + * @param accountsToBlacklist Accounts to re-add to the blacklist + */ + constructor( + FiatTokenProxy proxy, + FiatTokenV2_2 implementation, + address newProxyAdmin, + address[] memory accountsToBlacklist + ) public Ownable() { + _proxy = proxy; + _implementation = implementation; + _newProxyAdmin = newProxyAdmin; + _helper = new V2UpgraderHelper(address(proxy)); + _accountsToBlacklist = accountsToBlacklist; + } + + /** + * @notice The address of the FiatTokenProxy contract + * @return Contract address + */ + function proxy() external view returns (address) { + return address(_proxy); + } + + /** + * @notice The address of the FiatTokenV2_2 implementation contract + * @return Contract address + */ + function implementation() external view returns (address) { + return address(_implementation); + } + + /** + * @notice The address of the V2UpgraderHelper contract + * @return Contract address + */ + function helper() external view returns (address) { + return address(_helper); + } + + /** + * @notice The address to which the proxy admin role will be transferred + * after the upgrade is completed + * @return Address + */ + function newProxyAdmin() external view returns (address) { + return _newProxyAdmin; + } + + /** + * @notice Previously blacklisted accounts to re-add to the blacklist + * @return Address[] + */ + function accountsToBlacklist() external view returns (address[] memory) { + return _accountsToBlacklist; + } + + /** + * @notice Upgrade, transfer proxy admin role to a given address, run a + * sanity test, and tear down the upgrader contract, in a single atomic + * transaction. It rolls back if there is an error. + */ + function upgrade() external onlyOwner { + // The helper needs to be used to read contract state because + // AdminUpgradeabilityProxy does not allow the proxy admin to make + // proxy calls. + + // Check that this contract sufficient funds to run the tests + uint256 contractBal = _helper.balanceOf(address(this)); + require(contractBal >= 2e5, "V2_2Upgrader: 0.2 USDC needed"); + + uint256 callerBal = _helper.balanceOf(msg.sender); + + // Keep original contract metadata + string memory name = _helper.name(); + string memory symbol = _helper.symbol(); + uint8 decimals = _helper.decimals(); + string memory currency = _helper.currency(); + address masterMinter = _helper.masterMinter(); + address owner = _helper.fiatTokenOwner(); + address pauser = _helper.pauser(); + address blacklister = _helper.blacklister(); + + // Change implementation contract address + _proxy.upgradeTo(address(_implementation)); + + // Transfer proxy admin role + _proxy.changeAdmin(_newProxyAdmin); + + // Initialize V2_2 contract + FiatTokenV2_2 v2_2 = FiatTokenV2_2(address(_proxy)); + v2_2.initializeV2_2(_accountsToBlacklist); + + // Sanity test + // Check metadata + require( + keccak256(bytes(name)) == keccak256(bytes(v2_2.name())) && + keccak256(bytes(symbol)) == keccak256(bytes(v2_2.symbol())) && + decimals == v2_2.decimals() && + keccak256(bytes(currency)) == keccak256(bytes(v2_2.currency())) && + masterMinter == v2_2.masterMinter() && + owner == v2_2.owner() && + pauser == v2_2.pauser() && + blacklister == v2_2.blacklister(), + "V2_2Upgrader: metadata test failed" + ); + + // Test balanceOf + require( + v2_2.balanceOf(address(this)) == contractBal, + "V2_2Upgrader: balanceOf test failed" + ); + + // Test transfer + require( + v2_2.transfer(msg.sender, 1e5) && + v2_2.balanceOf(msg.sender) == callerBal.add(1e5) && + v2_2.balanceOf(address(this)) == contractBal.sub(1e5), + "V2_2Upgrader: transfer test failed" + ); + + // Test approve/transferFrom + require( + v2_2.approve(address(_helper), 1e5) && + v2_2.allowance(address(this), address(_helper)) == 1e5 && + _helper.transferFrom(address(this), msg.sender, 1e5) && + v2_2.allowance(address(this), msg.sender) == 0 && + v2_2.balanceOf(msg.sender) == callerBal.add(2e5) && + v2_2.balanceOf(address(this)) == contractBal.sub(2e5), + "V2_2Upgrader: approve/transferFrom test failed" + ); + + // Check that addresses that should be blacklisted are + for (uint256 i = 0; i < _accountsToBlacklist.length; i++) { + require(v2_2.isBlacklisted(_accountsToBlacklist[i]), "V2_2Upgrader: blacklist test failed"); + } + + // Transfer any remaining USDC to the caller + withdrawUSDC(); + + // Tear down + _helper.tearDown(); + selfdestruct(msg.sender); + } + + /** + * @notice Withdraw any USDC in the contract + */ + function withdrawUSDC() public onlyOwner { + IERC20 usdc = IERC20(address(_proxy)); + uint256 balance = usdc.balanceOf(address(this)); + if (balance > 0) { + require( + usdc.transfer(msg.sender, balance), + "V2_2Upgrader: failed to withdraw USDC" + ); + } + } + + /** + * @notice Transfer proxy admin role to newProxyAdmin, and self-destruct + */ + function abortUpgrade() external onlyOwner { + // Transfer proxy admin role + _proxy.changeAdmin(_newProxyAdmin); + + // Tear down + _helper.tearDown(); + selfdestruct(msg.sender); + } +} diff --git a/gas_usage_v2_1_vs_v2_2.js b/gas_usage_v2_1_vs_v2_2.js new file mode 100644 index 000000000..29f7c1652 --- /dev/null +++ b/gas_usage_v2_1_vs_v2_2.js @@ -0,0 +1,116 @@ +// run with `yarn truffle exec scripts/gas_usage_v2_1_vs_v2_2.js` + +const FiatTokenV2_1 = artifacts.require("FiatTokenV2_1"); +const FiatTokenV2_2 = artifacts.require("FiatTokenV2_2"); + +module.exports = async function (callback) { + try { + const [fiatTokenOwner, alice, bob] = await web3.eth.getAccounts(); + const mintAmount = 50e6; + const transferAmount = 10e6; + + const fiatTokenV2_2 = await initializeV2_2(fiatTokenOwner); + const fiatTokenV2_1 = await initializeV2_1(fiatTokenOwner); + + await FiatTokenV2_2.mint(alice, mintAmount, { from: fiatTokenOwner }); + const transferTxV2_2 = await FiatTokenV2_2.transfer(bob, transferAmount, { + from: alice, + }); + console.log("V2.2 transfer gas usage:", transferTxV2_2.receipt.gasUsed); + + await fiatTokenV2_1.mint(alice, mintAmount, { + from: fiatTokenOwner, + }); + const transferTxV2_1 = await fiatTokenV2_1.transfer(bob, transferAmount, { + from: alice, + }); + console.log("V2.1 transfer gas usage:", transferTxV2_1.receipt.gasUsed); + + const approvalTxV2_2 = await fiatTokenV2_2.approve(bob, transferAmount, { + from: alice, + }); + console.log("V2.2 approve gas usage:", approvalTxV2_2.receipt.gasUsed); + + const approvalTxV2_1 = await fiatTokenV2_1.approve(bob, transferAmount, { + from: alice, + }); + console.log("V2.1 approve gas usage:", approvalTxV2_1.receipt.gasUsed); + + const transferFromTxV2_2 = await fiatTokenV2_2.transferFrom( + alice, + bob, + transferAmount, + { + from: bob, + } + ); + console.log( + "V2.2 transferFrom gas usage:", + transferFromTxV2_2.receipt.gasUsed + ); + + const transferFromTxV2_1 = await fiatTokenV2_1.transferFrom( + alice, + bob, + transferAmount, + { + from: bob, + } + ); + console.log( + "V2.1 transferFrom gas usage:", + transferFromTxV2_1.receipt.gasUsed + ); + } catch (error) { + console.log(error); + } + + callback(); +}; + +async function initializeV2_2(fiatTokenOwner) { + const fiatTokenV2_2 = await FiatTokenV2_2.new(); + + await fiatTokenV2_2.initialize( + "USD Coin", + "USDC", + "USD", + 6, + fiatTokenOwner, + fiatTokenOwner, + fiatTokenOwner, + fiatTokenOwner + ); + await fiatTokenV2_2.initializeV2("USD Coin", { from: fiatTokenOwner }); + await fiatTokenV2_2.initializeV2_1(fiatTokenOwner, { from: fiatTokenOwner }); + await fiatTokenV2_2.initializeV2_2([], { from: fiatTokenOwner }); + + await fiatTokenV2_2.configureMinter(fiatTokenOwner, 1000000e6, { + from: fiatTokenOwner, + }); + + return fiatTokenV2_2; +} + +async function initializeV2_1(fiatTokenOwner) { + const fiatTokenV2_1 = await FiatTokenV2_1.new(); + + await fiatTokenV2_1.initialize( + "USD Coin", + "USDC", + "USD", + 6, + fiatTokenOwner, + fiatTokenOwner, + fiatTokenOwner, + fiatTokenOwner + ); + await fiatTokenV2_1.initializeV2("USD Coin", { from: fiatTokenOwner }); + await fiatTokenV2_1.initializeV2_1(fiatTokenOwner, { from: fiatTokenOwner }); + + await fiatTokenV2_1.configureMinter(fiatTokenOwner, 1000000e6, { + from: fiatTokenOwner, + }); + + return fiatTokenV2_1; +} diff --git a/migrations/8_deploy_v2_2.js b/migrations/8_deploy_v2_2.js new file mode 100644 index 000000000..5d3366a98 --- /dev/null +++ b/migrations/8_deploy_v2_2.js @@ -0,0 +1,48 @@ +const fs = require("fs"); +const path = require("path"); +const some = require("lodash/some"); + +const FiatTokenV2_2 = artifacts.require("FiatTokenV2_2"); +const FiatTokenProxy = artifacts.require("FiatTokenProxy"); + +const THROWAWAY_ADDRESS = "0x0000000000000000000000000000000000000001"; + +let proxyContractAddress = ""; + +// Read config file if it exists +if (fs.existsSync(path.join(__dirname, "..", "config.js"))) { + ({ PROXY_CONTRACT_ADDRESS: proxyContractAddress } = require("../config.js")); +} + +module.exports = async (deployer, network) => { + if ( + !proxyContractAddress || + some(["development", "coverage"], (v) => network.includes(v)) + ) { + proxyContractAddress = (await FiatTokenProxy.deployed()).address; + } + + console.log(`FiatTokenProxy: ${proxyContractAddress}`); + + console.log("Deploying FiatTokenV2_2 implementation contract..."); + await deployer.deploy(FiatTokenV2_2); + + const fiatTokenV2_2 = await FiatTokenV2_2.deployed(); + console.log("Deployed FiatTokenV2_2 at", fiatTokenV2_2.address); + console.log( + "Initializing FiatTokenV2_2 implementation contract with dummy values..." + ); + await fiatTokenV2_2.initialize( + "", + "", + "", + 0, + THROWAWAY_ADDRESS, + THROWAWAY_ADDRESS, + THROWAWAY_ADDRESS, + THROWAWAY_ADDRESS + ); + await fiatTokenV2_2.initializeV2(""); + await fiatTokenV2_2.initializeV2_1(THROWAWAY_ADDRESS); + await fiatTokenV2_2.initializeV2_2([]); +}; diff --git a/migrations/9_deploy_v2_2_upgrader.js b/migrations/9_deploy_v2_2_upgrader.js new file mode 100644 index 000000000..7d4150eb6 --- /dev/null +++ b/migrations/9_deploy_v2_2_upgrader.js @@ -0,0 +1,63 @@ +const fs = require("fs"); +const path = require("path"); +const some = require("lodash/some"); + +const FiatTokenV2_2 = artifacts.require("FiatTokenV2_2"); +const FiatTokenProxy = artifacts.require("FiatTokenProxy"); +const V2_2Upgrader = artifacts.require("V2_2Upgrader"); + +let proxyAdminAddress = ""; +let proxyContractAddress = ""; +let addressesToBlacklist = []; + +// Read config file if it exists +if (fs.existsSync(path.join(__dirname, "..", "config.js"))) { + ({ + PROXY_ADMIN_ADDRESS: proxyAdminAddress, + PROXY_CONTRACT_ADDRESS: proxyContractAddress, + ADDRESSES_TO_BLACKLIST: addressesToBlacklist, + } = require("../config.js")); +} + +module.exports = async (deployer, network) => { + if (some(["development", "coverage"], (v) => network.includes(v))) { + // DO NOT USE THESE ADDRESSES IN PRODUCTION + proxyAdminAddress = "0x2F560290FEF1B3Ada194b6aA9c40aa71f8e95598"; + proxyContractAddress = (await FiatTokenProxy.deployed()).address; + addressesToBlacklist = [ + "0xAa05F7C7eb9AF63D6cC03C36c4f4Ef6c37431EE0", + "0x7F367cC41522cE07553e823bf3be79A889DEbe1B", + ]; + } + proxyContractAddress = + proxyContractAddress || (await FiatTokenProxy.deployed()).address; + + if (addressesToBlacklist.length === 0) { + throw new Error("ADDRESSES_TO_BLACKLIST must be provided in config.js"); + } + + if (!proxyAdminAddress) { + throw new Error("PROXY_ADMIN_ADDRESS must be provided in config.js"); + } + + const fiatTokenV2_2 = await FiatTokenV2_2.deployed(); + + console.log(`Proxy Admin: ${proxyAdminAddress}`); + console.log(`FiatTokenProxy: ${proxyContractAddress}`); + console.log(`FiatTokenV2.2: ${fiatTokenV2_2.address}`); + console.log(`Addresses to Blacklist:`, addressesToBlacklist); + + console.log("Deploying V2_2Upgrader contract..."); + + const v2_2Upgrader = await deployer.deploy( + V2_2Upgrader, + proxyContractAddress, + fiatTokenV2_2.address, + proxyAdminAddress, + addressesToBlacklist + ); + + console.log( + `>>>>>>> Deployed V2_2Upgrader at ${v2_2Upgrader.address} <<<<<<<` + ); +}; diff --git a/scripts/blacklisted_addresses.json b/scripts/blacklisted_addresses.json new file mode 100644 index 000000000..6a0ae30bd --- /dev/null +++ b/scripts/blacklisted_addresses.json @@ -0,0 +1,46 @@ +{ + "asOfBlock": 15165191, + "blacklistedAddress": [ + "0xAa05F7C7eb9AF63D6cC03C36c4f4Ef6c37431EE0", + "0x7F367cC41522cE07553e823bf3be79A889DEbe1B", + "0x1da5821544e25c636c1417Ba96Ade4Cf6D2f9B5A", + "0x7Db418b5D567A4e0E8c59Ad71BE1FcE48f3E6107", + "0x72a5843cc08275C8171E582972Aa4fDa8C397B2A", + "0x7F19720A857F834887FC9A7bC0a0fBe7Fc7f8102", + "0xd882cFc20F52f2599D84b8e8D58C7FB62cfE344b", + "0x9F4cda013E354b8fC285BF4b9A60460cEe7f7Ea9", + "0x308eD4B7b49797e1A98D3818bFF6fe5385410370", + "0xe7aa314c77F4233C18C6CC84384A9247c0cf367B", + "0x19Aa5Fe80D33a56D56c78e82eA5E50E5d80b4Dff", + "0x2f389cE8bD8ff92De3402FFCe4691d17fC4f6535", + "0xA7e5d5A720f06526557c513402f2e6B5fA20b008", + "0x3CBdeD43EFdAf0FC77b9C55F6fC9988fCC9b757d", + "0x67d40EE1A85bf4a4Bb7Ffae16De985e8427B6b45", + "0x6F1cA141A28907F78Ebaa64fb83A9088b02A8352", + "0x6aCDFBA02D390b97Ac2b2d42A63E85293BCc160e", + "0x48549A34AE37b12F6a30566245176994e17C6b4A", + "0x5512d943eD1f7c8a43F3435C85F7aB68b30121b0", + "0xC455f7fd3e0e12afd51fba5c106909934D8A0e4a", + "0xfae5a6D3bD9BD24a3ED2f2A8A6031C83976c19a2", + "0x5EB95F30BD4409CFAADEBa75cD8d9c2CE4ED992A", + "0x461270bD08dfA98EdEc980345fD56D578a2d8F49", + "0xfEC8A60023265364D066a1212fDE3930F6Ae8da7", + "0x8576aCC5C05D6Ce88f4e49bf65BdF0C62F91353C", + "0x901bb9583b24D97e995513C6778dc6888AB6870e", + "0x7FF9cFad3877F21d41Da833E2F775dB0569eE3D9", + "0x098B716B8Aaf21512996dC57EB0615e2383E2f96", + "0xa0e1c89Ef1a489c9C7dE96311eD5Ce5D32c20E4B", + "0x3Cffd56B47B7b41c56258D9C7731ABaDc360E073", + "0x53b6936513e738f44FB50d2b9476730C0Ab3Bfc1", + "0xcce63fD31e9053c110c74CEbc37C8e358A6AA5bD", + "0x35fB6f6DB4fb05e6A4cE86f2C93691425626d4b1", + "0xF7B31119c2682c88d88D455dBb9d5932c65Cf1bE", + "0x3e37627dEAA754090fBFbb8bd226c1CE66D255e9", + "0x08723392Ed15743cc38513C4925f5e6be5c17243", + "0x29875bd49350aC3f2Ca5ceEB1c1701708c795FF3", + "0x06cAA9a5FD7E3Dc3b3157973455CbE9B9c2B14D2", + "0x2d66370666d7b9315E6e7fdb47f41aD722279833", + "0x9ff43BD969e8dbc383D1Aca50584c14266f3d876", + "0xBFd88175E4aE6f7f2EE4B01Bf96Cf48D2bCb4196" + ] +} diff --git a/scripts/get_blacklisted_addresses.js b/scripts/get_blacklisted_addresses.js new file mode 100644 index 000000000..5c4d7c1d0 --- /dev/null +++ b/scripts/get_blacklisted_addresses.js @@ -0,0 +1,103 @@ +// run with `yarn truffle exec scripts/get_blacklisted_addresses.js --network mainnet` +const fs = require("fs"); + +const FiatTokenV2_1 = require("../build/contracts/FiatTokenV2_1.json"); +const mainnetAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; +const originBlock = 6082465; +const queryWindow = 30000; + +module.exports = async function (callback) { + try { + const fiatTokenV2_1 = new web3.eth.Contract( + FiatTokenV2_1.abi, + mainnetAddress + ); + const currentBlock = await web3.eth.getBlockNumber(); + + const blacklistedEvents = []; + let fromBlock = originBlock; + let toBlock = originBlock + queryWindow; + while (toBlock < currentBlock) { + console.log(`querying events from ${fromBlock} to ${toBlock}`); + const events = await grabEventsWithRetryAsync( + fiatTokenV2_1, + 2, + fromBlock, + toBlock + ); + + if (events.length > 0) { + console.log("found events!"); + blacklistedEvents.push(...events); + events.map((event) => + console.log(event.blockNumber, event.returnValues._account) + ); + } + + fromBlock = toBlock + 1; + toBlock = Math.min(currentBlock, toBlock + queryWindow); + + // sleep for 200 ms + await new Promise((resolve) => setTimeout(resolve, 200)); + } + + const currentlyBlacklistedAddresses = []; + + // for each blacklist event, make sure the address is still blacklisted + for (const blacklistedEvent of blacklistedEvents) { + const isCurrentlyBlacklisted = await fiatTokenV2_1.methods + .isBlacklisted(blacklistedEvent.returnValues._account) + .call(); + + if (isCurrentlyBlacklisted) { + currentlyBlacklistedAddresses.push( + blacklistedEvent.returnValues._account + ); + } + } + + console.log("Currently Blacklisted Addresses:"); + console.log(currentlyBlacklistedAddresses); + + const outputJson = { + asOfBlock: currentBlock, + blacklistedAddress: currentlyBlacklistedAddresses, + }; + fs.writeFileSync( + "scripts/blacklisted_addresses.json", + JSON.stringify(outputJson), + "utf8", + (err) => { + if (err) throw err; + console.log("blacklist file saved"); + } + ); + } catch (error) { + console.log(error); + } + + callback(); +}; + +async function grabEventsWithRetryAsync( + contract, + maxRetries, + fromBlock, + toBlock +) { + let tries = 0; + + while (tries < maxRetries) { + try { + return await contract.getPastEvents("Blacklisted", { + fromBlock, + toBlock, + }); + } catch (e) { + console.log(e); + tries += 1; + } + } + + throw new Error("retries exceeded"); +} diff --git a/test/helpers/storageSlots.behavior.ts b/test/helpers/storageSlots.behavior.ts index 89de422cd..46f9eedea 100644 --- a/test/helpers/storageSlots.behavior.ts +++ b/test/helpers/storageSlots.behavior.ts @@ -13,7 +13,7 @@ export function usesOriginalStorageSlotPositions< accounts, }: { Contract: Truffle.Contract; - version: 1 | 1.1 | 2 | 2.1; + version: 1 | 1.1 | 2 | 2.1 | 2.2; accounts: Truffle.Accounts; }): void { describe("uses original storage slot positions", () => { @@ -134,19 +134,39 @@ export function usesOriginalStorageSlotPositions< } it("retains original storage slots for blacklisted mapping", async () => { - // blacklisted[alice] - let v = parseInt( - await readSlot(proxy.address, addressMappingSlot(alice, 3)), - 16 - ); - expect(v).to.equal(0); - - // blacklisted[charlie] - v = parseInt( - await readSlot(proxy.address, addressMappingSlot(charlie, 3)), - 16 - ); - expect(v).to.equal(1); + if (version < 2.2) { + // blacklisted[alice] + let v = parseInt( + await readSlot(proxy.address, addressMappingSlot(alice, 3)), + 16 + ); + expect(v).to.equal(0); + + // blacklisted[charlie] + v = parseInt( + await readSlot(proxy.address, addressMappingSlot(charlie, 3)), + 16 + ); + expect(v).to.equal(1); + } else { + // balances[alice] high bit + let v = parseInt( + await readSlot(proxy.address, addressMappingSlot(alice, 9)), + 16 + ) + .toString(2) + .padStart(256, "0")[0]; + expect(v).to.equal("0"); + + // balances[charlie] high bit + v = parseInt( + await readSlot(proxy.address, addressMappingSlot(charlie, 9)), + 16 + ) + .toString(2) + .padStart(256, "0")[0]; + expect(v).to.equal("1"); + } }); it("retains original storage slots for balances mapping", async () => { diff --git a/test/minting/MintP0_ArgumentTests.js b/test/minting/MintP0_ArgumentTests.js index 7bce73e2b..f6807075d 100644 --- a/test/minting/MintP0_ArgumentTests.js +++ b/test/minting/MintP0_ArgumentTests.js @@ -22,11 +22,11 @@ const zeroAddress = "0x0000000000000000000000000000000000000000"; const maxAmount = "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; -async function run_tests_MintController(newToken, accounts) { +async function run_tests_MintController(newToken, accounts, _version) { run_MINT_tests(newToken, MintController, accounts); } -async function run_tests_MasterMinter(newToken, accounts) { +async function run_tests_MasterMinter(newToken, accounts, _version) { run_MINT_tests(newToken, MasterMinter, accounts); } diff --git a/test/minting/MintP0_BasicTests.js b/test/minting/MintP0_BasicTests.js index d2d058ebb..d46b9bd19 100644 --- a/test/minting/MintP0_BasicTests.js +++ b/test/minting/MintP0_BasicTests.js @@ -22,11 +22,11 @@ const initializeTokenWithProxyAndMintController = const zeroAddress = "0x0000000000000000000000000000000000000000"; -async function run_tests_MintController(newToken, accounts) { +async function run_tests_MintController(newToken, accounts, _version) { run_MINT_tests(newToken, MintController, accounts); } -async function run_tests_MasterMinter(newToken, accounts) { +async function run_tests_MasterMinter(newToken, accounts, _version) { run_MINT_tests(newToken, MasterMinter, accounts); } diff --git a/test/minting/MintP0_EndToEndTests.js b/test/minting/MintP0_EndToEndTests.js index 9de45a70a..e2514c5dd 100644 --- a/test/minting/MintP0_EndToEndTests.js +++ b/test/minting/MintP0_EndToEndTests.js @@ -23,11 +23,11 @@ let token; let rawToken; let tokenConfig; -async function run_tests_MintController(newToken, accounts) { +async function run_tests_MintController(newToken, accounts, _version) { run_MINT_tests(newToken, MintController, accounts); } -async function run_tests_MasterMinter(newToken, accounts) { +async function run_tests_MasterMinter(newToken, accounts, _version) { run_MINT_tests(newToken, MasterMinter, accounts); } diff --git a/test/minting/MintP0_EventsTests.js b/test/minting/MintP0_EventsTests.js index 0ea216719..4bd9efd18 100644 --- a/test/minting/MintP0_EventsTests.js +++ b/test/minting/MintP0_EventsTests.js @@ -23,11 +23,11 @@ const mintControllerEvents = { minterAllowanceDecremented: "MinterAllowanceDecremented", }; -async function run_tests_MintController(newToken, accounts) { +async function run_tests_MintController(newToken, accounts, _version) { run_MINT_tests(newToken, MintController, accounts); } -async function run_tests_MasterMinter(newToken, accounts) { +async function run_tests_MasterMinter(newToken, accounts, _version) { run_MINT_tests(newToken, MasterMinter, accounts); } diff --git a/test/v1/abiHacking.test.js b/test/v1/abiHacking.test.js index 14f02e8ad..473a9f355 100644 --- a/test/v1/abiHacking.test.js +++ b/test/v1/abiHacking.test.js @@ -26,7 +26,7 @@ function mockStringAddressEncode(methodName, address) { return functionSignature(methodName) + version + encodeAddress(address); } -function runTests(newToken, _accounts) { +function runTests(newToken, _accounts, _version) { let proxy, token; beforeEach(async () => { diff --git a/test/v1/events.test.js b/test/v1/events.test.js index 4926d3690..5bad57305 100644 --- a/test/v1/events.test.js +++ b/test/v1/events.test.js @@ -32,7 +32,7 @@ const { const amount = 100; -function runTests(_newToken, _accounts) { +function runTests(_newToken, _accounts, _version) { let proxy, token; beforeEach(async () => { diff --git a/test/v1/extendedPositive.test.js b/test/v1/extendedPositive.test.js index 8603290f0..cd5faa0dc 100644 --- a/test/v1/extendedPositive.test.js +++ b/test/v1/extendedPositive.test.js @@ -11,12 +11,13 @@ const { pauserAccount, initializeTokenWithProxy, UpgradedFiatToken, + UpgradedFiatTokenV2_2, upgradeTo, } = require("./helpers/tokenTest"); const amount = 100; -function runTests(newToken, _accounts) { +function runTests(newToken, _accounts, _version) { let proxy, token; beforeEach(async () => { @@ -248,7 +249,13 @@ function runTests(newToken, _accounts) { it("ept022 should upgrade when msg.sender blacklisted", async () => { await token.blacklist(upgraderAccount, { from: blacklisterAccount }); - const newRawToken = await UpgradedFiatToken.new(); + let newRawToken; + if (_version < 2.2) { + newRawToken = await UpgradedFiatToken.new(); + } else { + newRawToken = await UpgradedFiatTokenV2_2.new(); + } + const tokenConfig = await upgradeTo(proxy, newRawToken); const newProxiedToken = tokenConfig.token; @@ -260,7 +267,12 @@ function runTests(newToken, _accounts) { }); it("ept023 should upgrade to blacklisted address", async () => { - const newRawToken = await UpgradedFiatToken.new(); + let newRawToken; + if (_version < 2.2) { + newRawToken = await UpgradedFiatToken.new(); + } else { + newRawToken = await UpgradedFiatTokenV2_2.new(); + } await token.blacklist(newRawToken.address, { from: blacklisterAccount }); const tokenConfig = await upgradeTo(proxy, newRawToken); diff --git a/test/v1/helpers/tokenTest.js b/test/v1/helpers/tokenTest.js index a223104b1..8602346b6 100644 --- a/test/v1/helpers/tokenTest.js +++ b/test/v1/helpers/tokenTest.js @@ -6,9 +6,13 @@ const Q = require("q"); const FiatTokenV1 = artifacts.require("FiatTokenV1"); const UpgradedFiatToken = artifacts.require("UpgradedFiatToken"); +const UpgradedFiatTokenV2_2 = artifacts.require("UpgradedFiatTokenV2_2"); const UpgradedFiatTokenNewFields = artifacts.require( "UpgradedFiatTokenNewFieldsTest" ); +const UpgradedFiatTokenNewFieldsV2_2 = artifacts.require( + "UpgradedFiatTokenNewFieldsV2_2Test" +); const UpgradedFiatTokenNewFieldsNewLogic = artifacts.require( "UpgradedFiatTokenNewFieldsNewLogicTest" ); @@ -963,7 +967,9 @@ module.exports = { FiatTokenV1, FiatTokenProxy, UpgradedFiatToken, + UpgradedFiatTokenV2_2, UpgradedFiatTokenNewFields, + UpgradedFiatTokenNewFieldsV2_2, UpgradedFiatTokenNewFieldsNewLogic, name, symbol, diff --git a/test/v1/helpers/wrapTests.js b/test/v1/helpers/wrapTests.js index 34f2bdc14..fec1e1e7f 100644 --- a/test/v1/helpers/wrapTests.js +++ b/test/v1/helpers/wrapTests.js @@ -1,6 +1,7 @@ const FiatTokenV1 = artifacts.require("FiatTokenV1"); const FiatTokenV1_1 = artifacts.require("FiatTokenV1_1"); const FiatTokenV2 = artifacts.require("FiatTokenV2"); +const FiatTokenV2_2 = artifacts.require("FiatTokenV2_2"); // The following helpers make fresh original/upgraded tokens before each test. @@ -16,19 +17,27 @@ function newFiatTokenV2() { return FiatTokenV2.new(); } +function newFiatTokenV2_2() { + return FiatTokenV2_2.new(); +} + // Executes the run_tests_function using an original and // an upgraded token. The test_suite_name is printed standard output. function wrapTests(testSuiteName, runTestsFunction) { contract(`FiatTokenV1: ${testSuiteName}`, (accounts) => { - runTestsFunction(newFiatTokenV1, accounts); + runTestsFunction(newFiatTokenV1, accounts, 1); }); contract(`FiatTokenV1_1: ${testSuiteName}`, (accounts) => { - runTestsFunction(newFiatTokenV1_1, accounts); + runTestsFunction(newFiatTokenV1_1, accounts, 1.1); }); contract(`FiatTokenV2: ${testSuiteName}`, (accounts) => { - runTestsFunction(newFiatTokenV2, accounts); + runTestsFunction(newFiatTokenV2, accounts, 2); + }); + + contract(`FiatTokenV2_2: ${testSuiteName}`, (accounts) => { + runTestsFunction(newFiatTokenV2_2, accounts, 2.2); }); } diff --git a/test/v1/legacy.test.js b/test/v1/legacy.test.js index a4373a93a..9d2212061 100644 --- a/test/v1/legacy.test.js +++ b/test/v1/legacy.test.js @@ -745,4 +745,6 @@ function runTests(_newToken, accounts) { }); } +// NOTE: these tests are run for each version but just +// use the v1 contract each time wrapTests("legacy", runTests); diff --git a/test/v1/misc.test.js b/test/v1/misc.test.js index db9f82ab1..59041793d 100644 --- a/test/v1/misc.test.js +++ b/test/v1/misc.test.js @@ -15,12 +15,14 @@ const { FiatTokenProxy, } = require("./helpers/tokenTest"); +// When using the high bit for blacklisting, 2^255 is the max +// usable amount const maxAmount = - "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; const maxAmountBN = new BN(maxAmount.slice(2), 16); const amount = 100; -function runTests(newToken, _accounts) { +function runTests(newToken, _accounts, _version) { let proxy, token; beforeEach(async () => { @@ -722,7 +724,7 @@ function runTests(newToken, _accounts) { assert.strictEqual("0x00", initialized); }); - it("ms047 configureMinter works on amount=2^256-1", async () => { + it("ms047 configureMinter works on amount=2^255-1", async () => { await token.configureMinter(minterAccount, maxAmount, { from: masterMinterAccount, }); @@ -736,7 +738,7 @@ function runTests(newToken, _accounts) { await checkVariables([token], [customVars]); }); - it("ms048 mint works on amount=2^256-1", async () => { + it("ms048 mint works on amount=2^255-1", async () => { await token.configureMinter(minterAccount, maxAmount, { from: masterMinterAccount, }); @@ -765,7 +767,7 @@ function runTests(newToken, _accounts) { await checkVariables([token], [customVars]); }); - it("ms049 burn on works on amount=2^256-1", async () => { + it("ms049 burn on works on amount=2^255-1", async () => { await token.configureMinter(minterAccount, maxAmount, { from: masterMinterAccount, }); @@ -787,7 +789,7 @@ function runTests(newToken, _accounts) { await checkVariables([token], [customVars]); }); - it("ms050 approve works on amount=2^256-1", async () => { + it("ms050 approve works on amount=2^255-1", async () => { await token.configureMinter(minterAccount, maxAmount, { from: masterMinterAccount, }); @@ -818,7 +820,7 @@ function runTests(newToken, _accounts) { await checkVariables([token], [customVars]); }); - it("ms051 transfer works on amount=2^256-1", async () => { + it("ms051 transfer works on amount=2^255-1", async () => { await token.configureMinter(minterAccount, maxAmount, { from: masterMinterAccount, }); @@ -845,7 +847,7 @@ function runTests(newToken, _accounts) { await checkVariables([token], [customVars]); }); - it("ms052 transferFrom works on amount=2^256-1", async () => { + it("ms052 transferFrom works on amount=2^255-1", async () => { await token.configureMinter(minterAccount, maxAmount, { from: masterMinterAccount, }); diff --git a/test/v1/negative.test.js b/test/v1/negative.test.js index d443791a2..9f8fd0bcf 100644 --- a/test/v1/negative.test.js +++ b/test/v1/negative.test.js @@ -19,7 +19,7 @@ const { const amount = 100; -function runTests(newToken, _accounts) { +function runTests(newToken, _accounts, _version) { let proxy, token; beforeEach(async () => { diff --git a/test/v1/positive.test.js b/test/v1/positive.test.js index 6e239b6ca..0b0dcb61c 100644 --- a/test/v1/positive.test.js +++ b/test/v1/positive.test.js @@ -14,7 +14,7 @@ const { const amount = 100; -function runTests(newToken, _accounts) { +function runTests(newToken, _accounts, _version) { let proxy, token; beforeEach(async () => { diff --git a/test/v1/proxyNegative.test.js b/test/v1/proxyNegative.test.js index 65363b76b..7209fc7c8 100644 --- a/test/v1/proxyNegative.test.js +++ b/test/v1/proxyNegative.test.js @@ -25,7 +25,7 @@ const { const amount = 100; -function runTests(newToken, _accounts) { +function runTests(newToken, _accounts, _version) { let rawToken, proxy, token; beforeEach(async () => { diff --git a/test/v1/proxyPositive.test.js b/test/v1/proxyPositive.test.js index 9b128bb73..cbecdeaaa 100644 --- a/test/v1/proxyPositive.test.js +++ b/test/v1/proxyPositive.test.js @@ -23,6 +23,7 @@ const { FiatTokenProxy, UpgradedFiatToken, UpgradedFiatTokenNewFields, + UpgradedFiatTokenNewFieldsV2_2, UpgradedFiatTokenNewFieldsNewLogic, getAdmin, } = require("./helpers/tokenTest"); @@ -30,7 +31,7 @@ const { makeRawTransaction, sendRawTransaction } = require("./helpers/abi"); const amount = 100; -function runTests(newToken, _accounts) { +function runTests(newToken, _accounts, _version) { let rawToken, proxy, token; beforeEach(async () => { @@ -405,7 +406,12 @@ function runTests(newToken, _accounts) { it("upt012 should upgradeToAndCall while upgrader is blacklisted", async () => { await token.blacklist(proxyOwnerAccount, { from: blacklisterAccount }); - const upgradedToken = await UpgradedFiatTokenNewFields.new(); + let upgradedToken; + if (_version < 2.2) { + upgradedToken = await UpgradedFiatTokenNewFields.new(); + } else { + upgradedToken = await UpgradedFiatTokenNewFieldsV2_2.new(); + } const initializeData = encodeCall( "initV2", ["bool", "address", "uint256"], diff --git a/test/v2/FiatTokenV2_2.test.ts b/test/v2/FiatTokenV2_2.test.ts new file mode 100644 index 000000000..84d2da935 --- /dev/null +++ b/test/v2/FiatTokenV2_2.test.ts @@ -0,0 +1,180 @@ +import BN from "bn.js"; + +import { FiatTokenV22Instance } from "../../@types/generated"; +import { usesOriginalStorageSlotPositions } from "../helpers/storageSlots.behavior"; +import { expectRevert } from "../helpers"; +import { makeDomainSeparator } from "../v2/GasAbstraction/helpers"; +import { MAX_UINT256 } from "../helpers/constants"; +import { assert, expect } from "chai"; + +const FiatTokenV2_2 = artifacts.require("FiatTokenV2_2"); + +// 2^255 - 1 is the max token supply +const maxTotalSupply = + "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; +const maxTotalSupplyBN = new BN(maxTotalSupply.slice(2), 16); +const maxUint256 = + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + +contract("FiatTokenV2_2", (accounts) => { + const fiatTokenOwner = accounts[0]; + const blacklister = accounts[4]; + let fiatToken: FiatTokenV22Instance; + const lostAndFound = accounts[2]; + const mintable = maxUint256; + const infiniteAllower = accounts[10]; + const infiniteSpender = accounts[11]; + const blacklist1 = accounts[12]; + const blacklist2 = accounts[13]; + + beforeEach(async () => { + fiatToken = await FiatTokenV2_2.new(); + await fiatToken.initialize( + "USD Coin", + "USDC", + "USD", + 6, + fiatTokenOwner, + fiatTokenOwner, + blacklister, + fiatTokenOwner + ); + await fiatToken.initializeV2("USD Coin", { from: fiatTokenOwner }); + await fiatToken.initializeV2_1(lostAndFound, { from: fiatTokenOwner }); + + await fiatToken.configureMinter(fiatTokenOwner, mintable, { + from: fiatTokenOwner, + }); + }); + + behavesLikeFiatTokenV2_2( + accounts, + () => fiatToken, + fiatTokenOwner, + infiniteAllower, + infiniteSpender, + [blacklist1, blacklist2] + ); +}); + +export function behavesLikeFiatTokenV2_2( + accounts: Truffle.Accounts, + getFiatToken: () => FiatTokenV2_2Instance, + fiatTokenOwner: string, + infiniteAllower: string, + infiniteSpender: string, + accountsToBlacklist: string[] +): void { + usesOriginalStorageSlotPositions({ + Contract: FiatTokenV2_2, + version: 3, + accounts, + }); + + it("has the expected domain separator", async () => { + await getFiatToken().initializeV2_2([], { from: fiatTokenOwner }); + const expectedDomainSeparator = makeDomainSeparator( + "USD Coin", + "2", + 1, // hardcoded to 1 because of ganache bug: https://github.com/trufflesuite/ganache/issues/1643 + getFiatToken().address + ); + expect(await getFiatToken().DOMAIN_SEPARATOR()).to.equal( + expectedDomainSeparator + ); + }); + + it("it allows user to set and remove an infinite allowance", async () => { + await getFiatToken().initializeV2_2([], { from: fiatTokenOwner }); + const maxAllowanceBN = new BN(MAX_UINT256.slice(2), 16); + const zeroBN = new BN(0); + + await getFiatToken().mint(infiniteAllower, 100e6, { + from: fiatTokenOwner, + }); + + await getFiatToken().approve(infiniteSpender, MAX_UINT256, { + from: infiniteAllower, + }); + + const allowanceAfterApprove = await getFiatToken().allowance( + infiniteAllower, + infiniteSpender + ); + assert.isTrue(allowanceAfterApprove.eq(maxAllowanceBN)); + + // spend allower's balance + await getFiatToken().transferFrom(infiniteAllower, infiniteSpender, 50e6, { + from: infiniteSpender, + }); + + const allowanceAfterSpend = await getFiatToken().allowance( + infiniteAllower, + infiniteSpender + ); + assert.isTrue(allowanceAfterSpend.eq(maxAllowanceBN)); + + // revoke approval + await getFiatToken().approve(infiniteSpender, 0, { from: infiniteAllower }); + const allowanceAfterRevoke = await getFiatToken().allowance( + infiniteAllower, + infiniteSpender + ); + assert.isTrue(allowanceAfterRevoke.eq(zeroBN)); + }); + + it("allows minting up to the maximum total supply", async () => { + await getFiatToken().initializeV2_2([], { from: fiatTokenOwner }); + + await getFiatToken().mint(infiniteAllower, maxTotalSupply, { + from: fiatTokenOwner, + }); + + assert.isTrue((await getFiatToken().totalSupply()).eq(maxTotalSupplyBN)); + }); + + it("disallows minting beyond maximum total supply", async () => { + await getFiatToken().initializeV2_2([], { from: fiatTokenOwner }); + + // mint max total supply + await getFiatToken().mint(infiniteAllower, maxTotalSupply, { + from: fiatTokenOwner, + }); + + // minting one more should fail + await expectRevert( + getFiatToken().mint(infiniteAllower, 1, { + from: fiatTokenOwner, + }), + "mint causes total supply to exceed supply cap" + ); + }); + + it("disallows calling initializeV2_2 twice", async () => { + await getFiatToken().initializeV2_2([], { from: fiatTokenOwner }); + + await expectRevert( + getFiatToken().initializeV2_2([], { from: fiatTokenOwner }) + ); + }); + + it("blacklists accounts passed into initializeV2_2", async () => { + await getFiatToken().initializeV2_2(accountsToBlacklist, { + from: fiatTokenOwner, + }); + + for (const account of accountsToBlacklist) { + expect(await getFiatToken().isBlacklisted(account)).to.eq(true); + } + }); + + it("initializeV2_2 blacklists the contract address itself", async () => { + await getFiatToken().initializeV2_2([], { + from: fiatTokenOwner, + }); + + expect(await getFiatToken().isBlacklisted(getFiatToken().address)).to.eq( + true + ); + }); +} diff --git a/test/v2/V2_2Upgrader.test.ts b/test/v2/V2_2Upgrader.test.ts new file mode 100644 index 000000000..7f73aaf26 --- /dev/null +++ b/test/v2/V2_2Upgrader.test.ts @@ -0,0 +1,259 @@ +import BN from "bn.js"; +import { + FiatTokenV2Instance, + FiatTokenV21Instance, + FiatTokenV22Instance, + FiatTokenProxyInstance, +} from "../../@types/generated"; +import { expectRevert, strip0x } from "../helpers"; +import { MAX_UINT256 } from "../helpers/constants"; + +const FiatTokenProxy = artifacts.require("FiatTokenProxy"); +const FiatTokenV2 = artifacts.require("FiatTokenV2"); +const FiatTokenV2_1 = artifacts.require("FiatTokenV2_1"); +const FiatTokenV2_2 = artifacts.require("FiatTokenV2_2"); +const V2_2Upgrader = artifacts.require("V2_2Upgrader"); + +const DEV_ACCOUNTS_TO_BLACKLIST = [ + "0xAa05F7C7eb9AF63D6cC03C36c4f4Ef6c37431EE0", + "0x7F367cC41522cE07553e823bf3be79A889DEbe1B", +]; + +contract("V2_2Upgrader", (accounts) => { + let fiatTokenProxy: FiatTokenProxyInstance; + let proxyAsV2_1: FiatTokenV21Instance; + let proxyAsV2_2: FiatTokenV22Instance; + let v2Implementation: FiatTokenV2Instance; + let v2_1Implementation: FiatTokenV21Instance; + let v2_2Implementation: FiatTokenV22Instance; + let originalProxyAdmin: string; + const [minter, alice, bob] = accounts.slice(9); + + before(async () => { + fiatTokenProxy = await FiatTokenProxy.deployed(); + proxyAsV2_1 = await FiatTokenV2_1.at(fiatTokenProxy.address); + proxyAsV2_2 = await FiatTokenV2_2.at(fiatTokenProxy.address); + v2Implementation = await FiatTokenV2.deployed(); + v2_1Implementation = await FiatTokenV2_1.deployed(); + v2_2Implementation = await FiatTokenV2_2.deployed(); + originalProxyAdmin = await fiatTokenProxy.admin(); + + // Upgrade from v1 to v2 + await fiatTokenProxy.upgradeToAndCall( + v2Implementation.address, + web3.eth.abi.encodeFunctionSignature("initializeV2(string)") + + strip0x(web3.eth.abi.encodeParameters(["string"], ["USD Coin"])), + { from: originalProxyAdmin } + ); + + // Upgrade from v2 to v2.1 + await fiatTokenProxy.upgradeToAndCall( + v2_1Implementation.address, + web3.eth.abi.encodeFunctionSignature("initializeV2_1(address)") + + strip0x( + web3.eth.abi.encodeParameters(["address"], [originalProxyAdmin]) + ), + { from: originalProxyAdmin } + ); + }); + + beforeEach(async () => { + await proxyAsV2_1.configureMinter(minter, 1000e6, { + from: await proxyAsV2_1.masterMinter(), + }); + await proxyAsV2_1.mint(minter, 100e6 + 2e5, { from: minter }); + }); + + describe("upgrade", () => { + it("upgrades, transfers proxy admin role to newProxyAdmin, runs tests, and self-destructs", async () => { + // Run the test on the contracts deployed by Truffle to ensure the Truffle + // migration is written correctly + const upgrader = await V2_2Upgrader.deployed(); + const upgraderOwner = await upgrader.owner(); + + expect(await upgrader.proxy()).to.equal(fiatTokenProxy.address); + expect(await upgrader.implementation()).to.equal( + v2_2Implementation.address + ); + expect(await upgrader.helper()).not.to.be.empty; + expect(await upgrader.newProxyAdmin()).to.equal(originalProxyAdmin); + expect(await upgrader.accountsToBlacklist()).to.deep.equal( + DEV_ACCOUNTS_TO_BLACKLIST + ); + + // Transfer 0.2 USDC to the contract + await proxyAsV2_2.transfer(upgrader.address, 2e5, { from: minter }); + + // Transfer admin role to the contract + await fiatTokenProxy.changeAdmin(upgrader.address, { + from: originalProxyAdmin, + }); + + // Call upgrade + await upgrader.upgrade({ from: upgraderOwner }); + + // The proxy admin role is transferred back to originalProxyAdmin + expect(await fiatTokenProxy.admin()).to.equal(originalProxyAdmin); + + // The implementation is updated to V2.2 + expect(await fiatTokenProxy.implementation()).to.equal( + v2_2Implementation.address + ); + + // mint works as expected + await proxyAsV2_2.configureMinter(minter, 1000e6, { + from: await proxyAsV2_2.masterMinter(), + }); + await proxyAsV2_2.mint(alice, 1000e6, { from: minter }); + expect((await proxyAsV2_2.balanceOf(alice)).toNumber()).to.equal(1000e6); + + await expectRevert( + proxyAsV2_2.mint(alice, 1, { from: alice }), + "caller is not a minter" + ); + + // transfer works as expected + await proxyAsV2_2.transfer(bob, 200e6, { from: alice }); + expect((await proxyAsV2_2.balanceOf(alice)).toNumber()).to.equal(800e6); + expect((await proxyAsV2_2.balanceOf(bob)).toNumber()).to.equal(200e6); + + // infinite allowance work as expected + const maxAllowanceBN = new BN(MAX_UINT256.slice(2), 16); + await proxyAsV2_2.approve(bob, MAX_UINT256, { from: alice }); + assert.isTrue( + (await proxyAsV2_2.allowance(alice, bob)).eq(maxAllowanceBN) + ); + await proxyAsV2_2.transferFrom(alice, bob, 250e6, { from: bob }); + assert.isTrue( + (await proxyAsV2_2.allowance(alice, bob)).eq(maxAllowanceBN) + ); + expect((await proxyAsV2_2.balanceOf(alice)).toNumber()).to.equal(550e6); + expect((await proxyAsV2_2.balanceOf(bob)).toNumber()).to.equal(450e6); + + // normal transferFrom works as expected + await proxyAsV2_2.approve(bob, 100e6, { from: alice }); + expect((await proxyAsV2_2.allowance(alice, bob)).toNumber()).to.equal( + 100e6 + ); + await proxyAsV2_2.transferFrom(alice, bob, 100e6, { from: bob }); + expect((await proxyAsV2_2.allowance(alice, bob)).toNumber()).to.equal(0); + expect((await proxyAsV2_2.balanceOf(alice)).toNumber()).to.equal(450e6); + expect((await proxyAsV2_2.balanceOf(bob)).toNumber()).to.equal(550e6); + + // Input accounts + proxy address itself are blacklisted' + const fiatProxyAddress = fiatTokenProxy.address; + const DEV_ACCOUNTS_TO_BLACKLIST_PLUS_FIAT_PROXY = [ + ...DEV_ACCOUNTS_TO_BLACKLIST, + fiatProxyAddress, + ]; + for (const account of DEV_ACCOUNTS_TO_BLACKLIST_PLUS_FIAT_PROXY) { + expect(await proxyAsV2_2.isBlacklisted(account)).to.equal(true); + await expectRevert( + proxyAsV2_2.mint(account, 1, { from: minter }), + "Blacklistable: account is blacklisted." + ); + await expectRevert( + proxyAsV2_2.transfer(account, 1, { from: alice }), + "Blacklistable: account is blacklisted." + ); + await expectRevert( + proxyAsV2_2.approve(account, 1, { from: alice }), + "Blacklistable: account is blacklisted." + ); + } + + // blacklisting functionality still works + // blacklisted accounts can't transfer funds out + const blacklister = await proxyAsV2_2.blacklister(); + await proxyAsV2_2.blacklist(alice, { from: blacklister }); + expect(await proxyAsV2_2.isBlacklisted(alice)).to.equal(true); + await expectRevert( + proxyAsV2_2.transfer(bob, 1, { from: alice }), + "Blacklistable: account is blacklisted." + ); + await proxyAsV2_2.unBlacklist(alice, { from: blacklister }); + expect(await proxyAsV2_2.isBlacklisted(alice)).to.equal(false); + + // burn works as expected + await proxyAsV2_2.transfer(minter, 100e6, { from: alice }); + expect((await proxyAsV2_2.balanceOf(minter)).toNumber()).to.equal(200e6); + await proxyAsV2_2.burn(200e6, { from: minter }); + expect((await proxyAsV2_2.balanceOf(minter)).toNumber()).to.equal(0); + + await expectRevert( + proxyAsV2_2.burn(1, { from: alice }), + "caller is not a minter" + ); + }); + + it("reverts if there is an error", async () => { + fiatTokenProxy = await FiatTokenProxy.new(v2_1Implementation.address, { + from: originalProxyAdmin, + }); + const fiatTokenV2_1 = await FiatTokenV2_1.new(); + const upgraderOwner = accounts[0]; + + const upgrader = await V2_2Upgrader.new( + fiatTokenProxy.address, + fiatTokenV2_1.address, // provide v2.1 implementation instead of v2.2 + originalProxyAdmin, + DEV_ACCOUNTS_TO_BLACKLIST, + { from: upgraderOwner } + ); + + // Transfer 0.2 USDC to the contract + await proxyAsV2_1.transfer(upgrader.address, 2e5, { from: minter }); + + // Transfer admin role to the contract + await fiatTokenProxy.changeAdmin(upgrader.address, { + from: originalProxyAdmin, + }); + + // Upgrade should fail because initializeV2_2 function doesn't exist on implementation + await expectRevert(upgrader.upgrade({ from: upgraderOwner }), "revert"); + + // The proxy admin role is not transferred + expect(await fiatTokenProxy.admin()).to.equal(upgrader.address); + + // The implementation is left unchanged + expect(await fiatTokenProxy.implementation()).to.equal( + v2_1Implementation.address + ); + }); + }); + + describe("abortUpgrade", () => { + it("transfers proxy admin role to newProxyAdmin and self-destructs", async () => { + fiatTokenProxy = await FiatTokenProxy.new(v2_1Implementation.address, { + from: originalProxyAdmin, + }); + const upgraderOwner = accounts[0]; + const upgrader = await V2_2Upgrader.new( + fiatTokenProxy.address, + v2_2Implementation.address, + originalProxyAdmin, + DEV_ACCOUNTS_TO_BLACKLIST, + { from: upgraderOwner } + ); + + // Transfer 0.2 USDC to the contract + await proxyAsV2_1.transfer(upgrader.address, 2e5, { from: minter }); + + // Transfer admin role to the contract + await fiatTokenProxy.changeAdmin(upgrader.address, { + from: originalProxyAdmin, + }); + + // Call abortUpgrade + await upgrader.abortUpgrade({ from: upgraderOwner }); + + // The proxy admin role is transferred back to originalProxyAdmin + expect(await fiatTokenProxy.admin()).to.equal(originalProxyAdmin); + + // The implementation is left unchanged + expect(await fiatTokenProxy.implementation()).to.equal( + v2_1Implementation.address + ); + }); + }); +});