diff --git a/contracts/v1/FiatTokenV1.sol b/contracts/v1/FiatTokenV1.sol index 1820842d1..f06485e0e 100644 --- a/contracts/v1/FiatTokenV1.sol +++ b/contracts/v1/FiatTokenV1.sol @@ -207,6 +207,7 @@ contract FiatTokenV1 is AbstractFiatTokenV1, Ownable, Pausable, Blacklistable { */ function approve(address spender, uint256 value) external + virtual override whenNotPaused notBlacklisted(msg.sender) @@ -247,6 +248,7 @@ contract FiatTokenV1 is AbstractFiatTokenV1, Ownable, Pausable, Blacklistable { uint256 value ) external + virtual override whenNotPaused notBlacklisted(msg.sender) @@ -271,6 +273,7 @@ contract FiatTokenV1 is AbstractFiatTokenV1, Ownable, Pausable, Blacklistable { */ function transfer(address to, uint256 value) external + virtual override whenNotPaused notBlacklisted(msg.sender) diff --git a/contracts/v2/FiatTokenV2.sol b/contracts/v2/FiatTokenV2.sol index 5535df3ba..45b5c7d62 100644 --- a/contracts/v2/FiatTokenV2.sol +++ b/contracts/v2/FiatTokenV2.sol @@ -58,6 +58,7 @@ contract FiatTokenV2 is FiatTokenV1_1, EIP3009, EIP2612 { */ function increaseAllowance(address spender, uint256 increment) external + virtual whenNotPaused notBlacklisted(msg.sender) notBlacklisted(spender) @@ -75,6 +76,7 @@ contract FiatTokenV2 is FiatTokenV1_1, EIP3009, EIP2612 { */ function decreaseAllowance(address spender, uint256 decrement) external + virtual whenNotPaused notBlacklisted(msg.sender) notBlacklisted(spender) @@ -106,7 +108,7 @@ contract FiatTokenV2 is FiatTokenV1_1, EIP3009, EIP2612 { uint8 v, bytes32 r, bytes32 s - ) external whenNotPaused notBlacklisted(from) notBlacklisted(to) { + ) external virtual whenNotPaused notBlacklisted(from) notBlacklisted(to) { _transferWithAuthorization( from, to, @@ -144,7 +146,7 @@ contract FiatTokenV2 is FiatTokenV1_1, EIP3009, EIP2612 { uint8 v, bytes32 r, bytes32 s - ) external whenNotPaused notBlacklisted(from) notBlacklisted(to) { + ) external virtual whenNotPaused notBlacklisted(from) notBlacklisted(to) { _receiveWithAuthorization( from, to, @@ -195,7 +197,13 @@ contract FiatTokenV2 is FiatTokenV1_1, EIP3009, EIP2612 { uint8 v, bytes32 r, bytes32 s - ) external whenNotPaused notBlacklisted(owner) notBlacklisted(spender) { + ) + external + virtual + whenNotPaused + notBlacklisted(owner) + notBlacklisted(spender) + { _permit(owner, spender, value, deadline, v, r, s); } diff --git a/contracts/v2/FiatTokenV2_1.sol b/contracts/v2/FiatTokenV2_1.sol index d7eaf65ef..e2ba71c87 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() external virtual view returns (string memory) { return "2"; } } diff --git a/contracts/v3/FiatTokenV3.sol b/contracts/v3/FiatTokenV3.sol new file mode 100644 index 000000000..3872783c7 --- /dev/null +++ b/contracts/v3/FiatTokenV3.sol @@ -0,0 +1,276 @@ +/** + * 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"; + +/** + * @title FiatToken + * @dev ERC20 Token backed by fiat reserves + */ +contract FiatTokenV3 is FiatTokenV2_1 { + mapping(address => uint256) internal frozenBalances; + + event BalanceFrozen(address indexed _account, uint256 amountFrozen); + event BalanceUnfrozen(address indexed _account, uint256 amountUnfrozen); + + /** + * @notice Initialize v3 + */ + function initializeV3() external { + require(_initializedVersion == 2, "v3 initialized out of order"); + + DOMAIN_SEPARATOR = EIP712.makeDomainSeparator(name, "3"); + + _initializedVersion = 3; + } + + /** + * @dev Freezes an account's balance + * @param _account The address whose balance will be frozen + */ + function freezeBalance(address _account) external onlyBlacklister { + uint256 amountFrozen = balances[_account]; + + totalSupply_ = totalSupply_.sub(amountFrozen); + frozenBalances[_account] = frozenBalances[_account].add(amountFrozen); + balances[_account] = 0; + + emit BalanceFrozen(_account, amountFrozen); + emit Transfer(_account, address(0), amountFrozen); + } + + /** + * @dev Unfreezes an account's balance + * @param _account The address whose balance will be unfrozen + */ + function unfreezeBalance(address _account) external onlyBlacklister { + uint256 amountUnfrozen = frozenBalances[_account]; + + totalSupply_ = totalSupply_.add(amountUnfrozen); + frozenBalances[_account] = 0; + balances[_account] = balances[_account].add(amountUnfrozen); + + emit BalanceUnfrozen(_account, amountUnfrozen); + emit Transfer(address(0), _account, amountUnfrozen); + } + + /** + * @dev Get frozen token balance of an account + * @param account address The account + */ + function frozenBalanceOf(address account) external view returns (uint256) { + return frozenBalances[account]; + } + + /** + * @notice Set spender's allowance over the caller's tokens to be a given + * value. + * @param spender Spender's address + * @param value Allowance amount + * @return True if successful + */ + function approve(address spender, uint256 value) + external + override + whenNotPaused + returns (bool) + { + _approve(msg.sender, spender, 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 + ) external override whenNotPaused returns (bool) { + require( + value <= allowed[from][msg.sender], + "ERC20: transfer amount exceeds allowance" + ); + _transfer(from, to, value); + allowed[from][msg.sender] = allowed[from][msg.sender].sub(value); + return true; + } + + /** + * @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 Increase the allowance by a given increment + * @param spender Spender's address + * @param increment Amount of increase in allowance + * @return True if successful + */ + function increaseAllowance(address spender, uint256 increment) + external + override + whenNotPaused + returns (bool) + { + _increaseAllowance(msg.sender, spender, increment); + return true; + } + + /** + * @notice Decrease the allowance by a given decrement + * @param spender Spender's address + * @param decrement Amount of decrease in allowance + * @return True if successful + */ + function decreaseAllowance(address spender, uint256 decrement) + external + override + whenNotPaused + returns (bool) + { + _decreaseAllowance(msg.sender, spender, decrement); + 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 + ) external override 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 + ) external override whenNotPaused { + _receiveWithAuthorization( + from, + to, + value, + validAfter, + validBefore, + nonce, + v, + r, + s + ); + } + + /** + * @notice Update allowance with a signed permit + * @param owner Token owner's address (Authorizer) + * @param spender Spender's address + * @param value Amount of allowance + * @param deadline Expiration time, seconds since the epoch + * @param v v of the signature + * @param r r of the signature + * @param s s of the signature + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external override whenNotPaused { + _permit(owner, spender, value, deadline, v, r, s); + } + + /** + * @notice Version string for the EIP712 domain separator + * @return Version string + */ + function version() external override view returns (string memory) { + return "3"; + } +} diff --git a/scripts/gas_usage_v2_vs_v3.js b/scripts/gas_usage_v2_vs_v3.js new file mode 100644 index 000000000..3b0383ce8 --- /dev/null +++ b/scripts/gas_usage_v2_vs_v3.js @@ -0,0 +1,111 @@ +const FiatTokenV2_1 = artifacts.require("FiatTokenV2_1"); +const FiatTokenV3 = artifacts.require("FiatTokenV3"); + +module.exports = async function (callback) { + try { + const [fiatTokenOwner, alice, bob] = await web3.eth.getAccounts(); + const mintAmount = 50e6; + const transferAmount = 10e6; + + const fiatTokenV3 = await initializeV3(fiatTokenOwner); + const fiatTokenV2_1 = await initializeV2_1(fiatTokenOwner); + + await fiatTokenV3.mint(alice, mintAmount, { from: fiatTokenOwner }); + const transferTxV3 = await fiatTokenV3.transfer(bob, transferAmount, { + from: alice, + }); + console.log("V3 transfer gas usage:", transferTxV3.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 approvalTxV3 = await fiatTokenV3.approve(bob, transferAmount, { + from: alice, + }); + console.log("V3 approve gas usage:", approvalTxV3.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 transferFromTxV3 = await fiatTokenV3.transferFrom( + alice, + bob, + transferAmount, + { + from: bob, + } + ); + console.log("V3 transferFrom gas usage:", transferFromTxV3.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 initializeV3(fiatTokenOwner) { + const fiatTokenV3 = await FiatTokenV3.new(); + + await fiatTokenV3.initialize( + "USD Coin", + "USDC", + "USD", + 6, + fiatTokenOwner, + fiatTokenOwner, + fiatTokenOwner, + fiatTokenOwner + ); + await fiatTokenV3.initializeV2("USD Coin", { from: fiatTokenOwner }); + await fiatTokenV3.initializeV2_1(fiatTokenOwner, { from: fiatTokenOwner }); + await fiatTokenV3.initializeV3({ from: fiatTokenOwner }); + + await fiatTokenV3.configureMinter(fiatTokenOwner, 1000000e6, { + from: fiatTokenOwner, + }); + + return fiatTokenV3; +} + +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/test/helpers/storageSlots.behavior.ts b/test/helpers/storageSlots.behavior.ts index 89de422cd..ea6095570 100644 --- a/test/helpers/storageSlots.behavior.ts +++ b/test/helpers/storageSlots.behavior.ts @@ -4,6 +4,7 @@ import { FiatTokenProxyInstance } from "../../@types/generated"; const FiatTokenProxy = artifacts.require("FiatTokenProxy"); const FiatTokenV1 = artifacts.require("FiatTokenV1"); const FiatTokenV1_1 = artifacts.require("FiatTokenV1_1"); +const FiatTokenV3 = artifacts.require("FiatTokenV3"); export function usesOriginalStorageSlotPositions< T extends Truffle.ContractInstance @@ -13,16 +14,17 @@ export function usesOriginalStorageSlotPositions< accounts, }: { Contract: Truffle.Contract; - version: 1 | 1.1 | 2 | 2.1; + version: 1 | 1.1 | 2 | 2.1 | 3; accounts: Truffle.Accounts; }): void { describe("uses original storage slot positions", () => { const [name, symbol, currency, decimals] = ["USD Coin", "USDC", "USD", 6]; - const [mintAllowance, minted, transferred, allowance] = [ + const [mintAllowance, minted, transferred, allowance, frozen] = [ 1000e6, 100e6, 30e6, 10e6, + 32e6, ]; const [ owner, @@ -35,6 +37,7 @@ export function usesOriginalStorageSlotPositions< alice, bob, charlie, + sketchyMcSketcherton, ] = accounts; let fiatToken: T; @@ -64,6 +67,9 @@ export function usesOriginalStorageSlotPositions< await proxyAsFiatTokenV1.transfer(bob, transferred, { from: alice }); await proxyAsFiatTokenV1.approve(charlie, allowance, { from: alice }); await proxyAsFiatTokenV1.blacklist(charlie, { from: blacklister }); + await proxyAsFiatTokenV1.mint(sketchyMcSketcherton, frozen, { + from: minter, + }); await proxyAsFiatTokenV1.pause({ from: pauser }); if (version >= 1.1) { @@ -72,6 +78,13 @@ export function usesOriginalStorageSlotPositions< from: owner, }); } + + if (version >= 3) { + const proxyAsFiatTokenV3 = await FiatTokenV3.at(proxy.address); + await proxyAsFiatTokenV3.freezeBalance(sketchyMcSketcherton, { + from: blacklister, + }); + } }); it("retains original storage slots 0 through 13", async () => { @@ -117,7 +130,12 @@ export function usesOriginalStorageSlotPositions< expect(slots[10]).to.equal("0"); // slot 11 - totalSupply - expect(parseUint(slots[11]).toNumber()).to.equal(minted); + if (version >= 3) { + // for version 3 some of the supply is frozen and removed + expect(parseUint(slots[11]).toNumber()).to.equal(minted); + } else { + expect(parseUint(slots[11]).toNumber()).to.equal(minted + frozen); + } // slot 12 - minters (mapping, slot is unused) expect(slots[12]).to.equal("0"); @@ -133,6 +151,25 @@ export function usesOriginalStorageSlotPositions< }); } + if (version >= 3) { + it("retains slot 19 for frozen balances", async () => { + const slot = await readSlot(proxy.address, 19); + expect(slot).to.equal("0"); + }); + + it("retains storage slots for frozen balances mapping", async () => { + const v = parseInt( + await readSlot( + proxy.address, + addressMappingSlot(sketchyMcSketcherton, 19) + ), + 16 + ); + + expect(v).to.equal(frozen); + }); + } + it("retains original storage slots for blacklisted mapping", async () => { // blacklisted[alice] let v = parseInt( @@ -202,7 +239,7 @@ export function usesOriginalStorageSlotPositions< await readSlot(proxy.address, addressMappingSlot(minter, 13)), 16 ); - expect(v).to.equal(mintAllowance - minted); + expect(v).to.equal(mintAllowance - minted - frozen); // minterAllowed[alice] v = parseInt( 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..3a233075b 100644 --- a/test/v1/extendedPositive.test.js +++ b/test/v1/extendedPositive.test.js @@ -16,7 +16,7 @@ const { const amount = 100; -function runTests(newToken, _accounts) { +function runTests(newToken, _accounts, _version) { let proxy, token; beforeEach(async () => { diff --git a/test/v1/helpers/wrapTests.js b/test/v1/helpers/wrapTests.js index 34f2bdc14..d45a270cf 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 FiatTokenV3 = artifacts.require("FiatTokenV3"); // The following helpers make fresh original/upgraded tokens before each test. @@ -16,19 +17,27 @@ function newFiatTokenV2() { return FiatTokenV2.new(); } +function newFiatTokenV3() { + return FiatTokenV3.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(`FiatTokenV3: ${testSuiteName}`, (accounts) => { + runTestsFunction(newFiatTokenV3, accounts, 3); }); } diff --git a/test/v1/legacy.test.js b/test/v1/legacy.test.js index a4373a93a..571e17365 100644 --- a/test/v1/legacy.test.js +++ b/test/v1/legacy.test.js @@ -32,7 +32,7 @@ const { } = require("./helpers/tokenTest"); // these tests are for reference and do not track side effects on all variables -function runTests(_newToken, accounts) { +function runTests(_newToken, accounts, _version) { let proxy, token; beforeEach(async () => { @@ -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..c89f0e17b 100644 --- a/test/v1/misc.test.js +++ b/test/v1/misc.test.js @@ -20,7 +20,7 @@ const maxAmount = 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 () => { diff --git a/test/v1/negative.test.js b/test/v1/negative.test.js index d443791a2..32a1c7ee6 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 () => { @@ -136,6 +136,9 @@ function runTests(newToken, _accounts) { // Approve it("nt008 should fail to approve when spender is blacklisted", async () => { + if (_version >= 3) { + return; + } await token.blacklist(minterAccount, { from: blacklisterAccount }); const customVars = [ { variable: "isAccountBlacklisted.minterAccount", expectedValue: true }, @@ -147,6 +150,9 @@ function runTests(newToken, _accounts) { }); it("nt009 should fail to approve when msg.sender is blacklisted", async () => { + if (_version >= 3) { + return; + } await token.blacklist(arbitraryAccount, { from: blacklisterAccount }); const customVars = [ { @@ -250,6 +256,9 @@ function runTests(newToken, _accounts) { }); it("nt014 should fail to transferFrom to blacklisted recipient", async () => { + if (_version >= 3) { + return; + } await token.configureMinter(minterAccount, amount, { from: masterMinterAccount, }); @@ -294,6 +303,9 @@ function runTests(newToken, _accounts) { }); it("nt015 should fail to transferFrom from blacklisted msg.sender", async () => { + if (_version >= 3) { + return; + } await token.configureMinter(minterAccount, amount, { from: masterMinterAccount, }); @@ -338,6 +350,9 @@ function runTests(newToken, _accounts) { }); it("nt016 should fail to transferFrom when from is blacklisted", async () => { + if (_version >= 3) { + return; + } await token.configureMinter(minterAccount, amount, { from: masterMinterAccount, }); @@ -528,6 +543,9 @@ function runTests(newToken, _accounts) { }); it("nt022 should fail to transfer to blacklisted recipient", async () => { + if (_version >= 3) { + return; + } await token.configureMinter(minterAccount, amount, { from: masterMinterAccount, }); @@ -565,6 +583,9 @@ function runTests(newToken, _accounts) { }); it("nt023 should fail to transfer when sender is blacklisted", async () => { + if (_version >= 3) { + return; + } await token.configureMinter(minterAccount, amount, { from: masterMinterAccount, }); 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..6ae9c72f8 100644 --- a/test/v1/proxyPositive.test.js +++ b/test/v1/proxyPositive.test.js @@ -30,7 +30,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 () => { diff --git a/test/v3/FiatTokenV3.test.ts b/test/v3/FiatTokenV3.test.ts new file mode 100644 index 000000000..556d96c73 --- /dev/null +++ b/test/v3/FiatTokenV3.test.ts @@ -0,0 +1,149 @@ +import { FiatTokenV3Instance } from "../../@types/generated"; +import { usesOriginalStorageSlotPositions } from "../helpers/storageSlots.behavior"; +import { expectRevert } from "../helpers"; +import { makeDomainSeparator } from "../v2/GasAbstraction/helpers"; +import { checkFreezeEvents, checkUnfreezeEvents } from "./utils"; + +const FiatTokenV3 = artifacts.require("FiatTokenV3"); + +contract("FiatTokenV3", (accounts) => { + const fiatTokenOwner = accounts[0]; + const blacklister = accounts[4]; + const sketchyUser = accounts[10]; + let fiatToken: FiatTokenV3Instance; + const lostAndFound = accounts[2]; + const sketchyBalance = 32e6; + const mintable = 1000000e6; + + beforeEach(async () => { + fiatToken = await FiatTokenV3.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.initializeV3({ from: fiatTokenOwner }); + + await fiatToken.configureMinter(fiatTokenOwner, mintable, { + from: fiatTokenOwner, + }); + await fiatToken.mint(sketchyUser, sketchyBalance, { from: fiatTokenOwner }); + }); + + behavesLikeFiatTokenV3( + accounts, + () => fiatToken, + fiatTokenOwner, + blacklister, + sketchyUser, + sketchyBalance + ); +}); + +export function behavesLikeFiatTokenV3( + accounts: Truffle.Accounts, + getFiatToken: () => FiatTokenV3Instance, + fiatTokenOwner: string, + blacklister: string, + sketchyUser: string, + sketchyBalance: number +): void { + usesOriginalStorageSlotPositions({ + Contract: FiatTokenV3, + version: 3, + accounts, + }); + + it("has the expected domain separator", async () => { + const expectedDomainSeparator = makeDomainSeparator( + "USD Coin", + "3", + 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("allows the blacklister to freeze and unfreeze an account", async () => { + const freezing = await getFiatToken().freezeBalance(sketchyUser, { + from: blacklister, + }); + checkFreezeEvents(freezing, sketchyBalance, sketchyUser); + + expect((await getFiatToken().balanceOf(sketchyUser)).toNumber()).to.equal( + 0 + ); + expect( + (await getFiatToken().frozenBalanceOf(sketchyUser)).toNumber() + ).to.equal(sketchyBalance); + + const unfreezing = await getFiatToken().unfreezeBalance(sketchyUser, { + from: blacklister, + }); + checkUnfreezeEvents(unfreezing, sketchyBalance, sketchyUser); + + expect((await getFiatToken().balanceOf(sketchyUser)).toNumber()).to.equal( + sketchyBalance + ); + expect( + (await getFiatToken().frozenBalanceOf(sketchyUser)).toNumber() + ).to.equal(0); + }); + + it("forbids a non-blacklister from freezing or unfreezing an account", async () => { + await expectRevert( + getFiatToken().freezeBalance(sketchyUser, { from: fiatTokenOwner }) + ); + expect((await getFiatToken().balanceOf(sketchyUser)).toNumber()).to.equal( + sketchyBalance + ); + expect( + (await getFiatToken().frozenBalanceOf(sketchyUser)).toNumber() + ).to.equal(0); + + await getFiatToken().freezeBalance(sketchyUser, { from: blacklister }); + + await expectRevert( + getFiatToken().unfreezeBalance(sketchyUser, { from: fiatTokenOwner }) + ); + + expect((await getFiatToken().balanceOf(sketchyUser)).toNumber()).to.equal( + 0 + ); + expect( + (await getFiatToken().frozenBalanceOf(sketchyUser)).toNumber() + ).to.equal(sketchyBalance); + + await getFiatToken().unfreezeBalance(sketchyUser, { from: blacklister }); + }); + + it("adds/subtracts unfrozen/frozen amounts from total supply", async () => { + expect((await getFiatToken().totalSupply()).toNumber()).to.equal( + sketchyBalance + ); + + await getFiatToken().freezeBalance(sketchyUser, { from: blacklister }); + + expect((await getFiatToken().totalSupply()).toNumber()).to.equal(0); + + await getFiatToken().unfreezeBalance(sketchyUser, { from: blacklister }); + + expect((await getFiatToken().totalSupply()).toNumber()).to.equal( + sketchyBalance + ); + }); + + it("disallows calling initializeV3 twice", async () => { + // It was called once in beforeEach. Try to call again. + await expectRevert(getFiatToken().initializeV3({ from: fiatTokenOwner })); + }); +} diff --git a/test/v3/utils.ts b/test/v3/utils.ts new file mode 100644 index 000000000..50d7461ad --- /dev/null +++ b/test/v3/utils.ts @@ -0,0 +1,37 @@ +import BN from "bn.js"; + +const nullAccount = "0x0000000000000000000000000000000000000000"; + +export function checkFreezeEvents( + freezing: Truffle.TransactionResponse, + amount: number, + targetAccount: string +): void { + // BalanceFrozen Event + assert.strictEqual(freezing.logs[0].event, "BalanceFrozen"); + assert.strictEqual(freezing.logs[0].args._account, targetAccount); + assert.isTrue(freezing.logs[0].args.amountFrozen.eq(new BN(amount))); + + // Transfer to 0 Event + assert.strictEqual(freezing.logs[1].event, "Transfer"); + assert.strictEqual(freezing.logs[1].args.from, targetAccount); + assert.strictEqual(freezing.logs[1].args.to, nullAccount); + assert.isTrue(freezing.logs[1].args.value.eq(new BN(amount))); +} + +export function checkUnfreezeEvents( + unfreezing: Truffle.TransactionResponse, + amount: number, + targetAccount: string +): void { + // BalanceUnfrozen Event + assert.strictEqual(unfreezing.logs[0].event, "BalanceUnfrozen"); + assert.strictEqual(unfreezing.logs[0].args._account, targetAccount); + assert.isTrue(unfreezing.logs[0].args.amountUnfrozen.eq(new BN(amount))); + + // Transfer from 0 Event + assert.strictEqual(unfreezing.logs[1].event, "Transfer"); + assert.strictEqual(unfreezing.logs[1].args.from, nullAccount); + assert.strictEqual(unfreezing.logs[1].args.to, targetAccount); + assert.isTrue(unfreezing.logs[1].args.value.eq(new BN(amount))); +} diff --git a/tsconfig.json b/tsconfig.json index 230509337..da821ccbb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,6 @@ "strict": true, "target": "ES2019" }, - "include": ["**/*.ts", "*.ts"], + "include": ["**/*.ts", "*.ts", "src/gas_usage_v2_vs_v3.js"], "exclude": ["node_modules", "build"] }