diff --git a/src/contracts/test/GPv2SafeERC20TestInterface.sol b/src/contracts/test/GPv2SafeERC20TestInterface.sol deleted file mode 100644 index ee9f5c10..00000000 --- a/src/contracts/test/GPv2SafeERC20TestInterface.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -pragma solidity >=0.7.6 <0.9.0; -pragma abicoder v2; - -import "../interfaces/IERC20.sol"; -import "../libraries/GPv2SafeERC20.sol"; - -contract GPv2SafeERC20TestInterface { - using GPv2SafeERC20 for IERC20; - - function transfer(IERC20 token, address to, uint256 value) public { - token.safeTransfer(to, value); - } - - function transferFrom( - IERC20 token, - address from, - address to, - uint256 value - ) public { - token.safeTransferFrom(from, to, value); - } -} diff --git a/test/GPv2SafeERC20.test.ts b/test/GPv2SafeERC20.test.ts deleted file mode 100644 index 9491b0b9..00000000 --- a/test/GPv2SafeERC20.test.ts +++ /dev/null @@ -1,255 +0,0 @@ -import IERC20 from "@openzeppelin/contracts/build/contracts/IERC20.json"; -import { expect } from "chai"; -import { Contract } from "ethers"; -import { artifacts, ethers, waffle } from "hardhat"; -import { Artifact } from "hardhat/types"; - -describe("GPv2SafeERC20.sol", () => { - const [deployer, recipient, ...traders] = waffle.provider.getWallets(); - - let executor: Contract; - - let ERC20NoReturn: Artifact; - let ERC20ReturningUint: Artifact; - - beforeEach(async () => { - const GPv2SafeERC20TestInterface = await ethers.getContractFactory( - "GPv2SafeERC20TestInterface", - ); - executor = await GPv2SafeERC20TestInterface.deploy(); - - ERC20NoReturn = await artifacts.readArtifact("ERC20NoReturn"); - ERC20ReturningUint = await artifacts.readArtifact("ERC20ReturningUint"); - }); - - describe("transfer", () => { - it("succeeds when the internal call succeeds", async () => { - const amount = ethers.utils.parseEther("13.37"); - - const sellToken = await waffle.deployMockContract(deployer, IERC20.abi); - await sellToken.mock.transfer - .withArgs(recipient.address, amount) - .returns(true); - - await expect( - executor.transfer(sellToken.address, recipient.address, amount), - ).to.not.be.reverted; - }); - - it("reverts on failed internal call", async () => { - const amount = ethers.utils.parseEther("4.2"); - - const sellToken = await waffle.deployMockContract(deployer, IERC20.abi); - await sellToken.mock.transfer - .withArgs(recipient.address, amount) - .revertsWithReason("test error"); - - await expect( - executor.transfer(sellToken.address, recipient.address, amount), - ).to.be.revertedWith("test error"); - }); - - describe("Non-Standard ERC20 Tokens", () => { - it("does not revert when the internal call has no return data", async () => { - const amount = ethers.utils.parseEther("13.37"); - - const sellToken = await waffle.deployMockContract( - deployer, - ERC20NoReturn.abi, - ); - await sellToken.mock.transfer - .withArgs(recipient.address, amount) - .returns(); - - await expect( - executor.transfer(sellToken.address, recipient.address, amount), - ).to.not.be.reverted; - }); - - it("reverts when the internal call returns false", async () => { - const amount = ethers.utils.parseEther("4.2"); - - const sellToken = await waffle.deployMockContract(deployer, IERC20.abi); - await sellToken.mock.transfer - .withArgs(recipient.address, amount) - .returns(false); - - await expect( - executor.transfer(sellToken.address, recipient.address, amount), - ).to.be.revertedWith("failed transfer"); - }); - - it("reverts when too much data is returned", async () => { - const amount = ethers.utils.parseEther("1.0"); - - const sellToken = await waffle.deployMockContract(deployer, [ - "function transfer(address, uint256) returns (bytes)", - ]); - await sellToken.mock.transfer - .withArgs(recipient.address, amount) - .returns(ethers.utils.hexlify([...Array(256)].map((_, i) => i))); - - await expect( - executor.transfer(sellToken.address, recipient.address, amount), - ).to.be.revertedWith("malformed transfer result"); - }); - - it("coerces invalid ABI encoded bool", async () => { - const amount = ethers.utils.parseEther("1.0"); - - const sellToken = await waffle.deployMockContract( - deployer, - ERC20ReturningUint.abi, - ); - await sellToken.mock.transfer - .withArgs(recipient.address, amount) - .returns(42); - - await expect( - executor.transfer(sellToken.address, recipient.address, amount), - ).to.not.be.reverted; - }); - }); - - it("reverts when calling a non-contract", async () => { - const amount = ethers.utils.parseEther("4.2"); - - await expect( - executor.transfer(traders[1].address, recipient.address, amount), - ).to.be.revertedWith("not a contract"); - }); - }); - - describe("transferFrom", () => { - it("succeeds when the internal call succeeds", async () => { - const amount = ethers.utils.parseEther("13.37"); - - const sellToken = await waffle.deployMockContract(deployer, IERC20.abi); - await sellToken.mock.transferFrom - .withArgs(traders[0].address, recipient.address, amount) - .returns(true); - - await expect( - executor.transferFrom( - sellToken.address, - traders[0].address, - recipient.address, - amount, - ), - ).to.not.be.reverted; - }); - - it("reverts on failed internal call", async () => { - const amount = ethers.utils.parseEther("4.2"); - - const sellToken = await waffle.deployMockContract(deployer, IERC20.abi); - await sellToken.mock.transferFrom - .withArgs(traders[0].address, recipient.address, amount) - .revertsWithReason("test error"); - - await expect( - executor.transferFrom( - sellToken.address, - traders[0].address, - recipient.address, - amount, - ), - ).to.be.revertedWith("test error"); - }); - - describe("Non-Standard ERC20 Tokens", () => { - it("does not revert when the internal call has no return data", async () => { - const amount = ethers.utils.parseEther("13.37"); - - const sellToken = await waffle.deployMockContract( - deployer, - ERC20NoReturn.abi, - ); - await sellToken.mock.transferFrom - .withArgs(traders[0].address, recipient.address, amount) - .returns(); - - await expect( - executor.transferFrom( - sellToken.address, - traders[0].address, - recipient.address, - amount, - ), - ).to.not.be.reverted; - }); - - it("reverts when the internal call returns false", async () => { - const amount = ethers.utils.parseEther("4.2"); - - const sellToken = await waffle.deployMockContract(deployer, IERC20.abi); - await sellToken.mock.transferFrom - .withArgs(traders[0].address, recipient.address, amount) - .returns(false); - - await expect( - executor.transferFrom( - sellToken.address, - traders[0].address, - recipient.address, - amount, - ), - ).to.be.revertedWith("failed transferFrom"); - }); - - it("reverts when too much data is returned", async () => { - const amount = ethers.utils.parseEther("1.0"); - - const sellToken = await waffle.deployMockContract(deployer, [ - "function transferFrom(address, address, uint256) returns (bytes)", - ]); - await sellToken.mock.transferFrom - .withArgs(traders[0].address, recipient.address, amount) - .returns(ethers.utils.hexlify([...Array(256)].map((_, i) => i))); - - await expect( - executor.transferFrom( - sellToken.address, - traders[0].address, - recipient.address, - amount, - ), - ).to.be.revertedWith("malformed transfer result"); - }); - - it("coerces invalid ABI encoded bool", async () => { - const amount = ethers.utils.parseEther("1.0"); - - const sellToken = await waffle.deployMockContract( - deployer, - ERC20ReturningUint.abi, - ); - await sellToken.mock.transferFrom - .withArgs(traders[0].address, recipient.address, amount) - .returns(42); - - await expect( - executor.transferFrom( - sellToken.address, - traders[0].address, - recipient.address, - amount, - ), - ).to.not.be.reverted; - }); - }); - - it("reverts when calling a non-contract", async () => { - const amount = ethers.utils.parseEther("4.2"); - - await expect( - executor.transferFrom( - traders[1].address, - traders[0].address, - recipient.address, - amount, - ), - ).to.be.revertedWith("not a contract"); - }); - }); -}); diff --git a/test/GPv2SafeERC20/Helper.sol b/test/GPv2SafeERC20/Helper.sol new file mode 100644 index 00000000..c59d6be6 --- /dev/null +++ b/test/GPv2SafeERC20/Helper.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; + +import {IERC20} from "src/contracts/interfaces/IERC20.sol"; +import {GPv2SafeERC20} from "src/contracts/libraries/GPv2SafeERC20.sol"; + +contract Harness { + using GPv2SafeERC20 for IERC20; + + function transfer(IERC20 token, address to, uint256 value) public { + token.safeTransfer(to, value); + } + + function transferFrom(IERC20 token, address from, address to, uint256 value) public { + token.safeTransferFrom(from, to, value); + } +} + +contract Helper is Test { + Harness executor; + address recipient = makeAddr("TestHelper: recipient"); + + function setUp() public { + executor = new Harness(); + } +} diff --git a/test/GPv2SafeERC20/Transfer.t.sol b/test/GPv2SafeERC20/Transfer.t.sol new file mode 100644 index 00000000..16f104c5 --- /dev/null +++ b/test/GPv2SafeERC20/Transfer.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8.0; + +import {IERC20} from "src/contracts/interfaces/IERC20.sol"; + +import {Helper} from "./Helper.sol"; + +contract Transfer is Helper { + function test_succeeds_when_internal_call_succeeds() public { + uint256 amount = 13.37 ether; + + IERC20 standardToken = IERC20(makeAddr("standard token")); + vm.mockCall(address(standardToken), abi.encodeCall(IERC20.transfer, (recipient, amount)), abi.encode(true)); + + executor.transfer(standardToken, recipient, amount); + } + + function test_reverts_on_failed_internal_call() public { + uint256 amount = 42 ether; + + IERC20 revertingToken = IERC20(makeAddr("reverting token")); + + vm.mockCallRevert( + address(revertingToken), abi.encodeCall(IERC20.transfer, (recipient, amount)), bytes("test error") + ); + + vm.expectRevert("test error"); + executor.transfer(revertingToken, recipient, amount); + } + + function test_reverts_when_calling_a_non_contract() public { + uint256 amount = 4.2 ether; + + IERC20 standardToken = IERC20(makeAddr("address without code")); + + vm.expectRevert("GPv2: not a contract"); + executor.transfer(standardToken, recipient, amount); + } + + function test_does_not_revert_when_the_internal_call_has_no_return_data() public { + uint256 amount = 13.37 ether; + + IERC20 tokenNoReturnValue = IERC20(makeAddr("does not return on transfer")); + + vm.mockCall(address(tokenNoReturnValue), abi.encodeCall(IERC20.transfer, (recipient, amount)), hex""); + + executor.transfer(tokenNoReturnValue, recipient, amount); + } + + function test_reverts_when_the_internal_call_returns_false() public { + uint256 amount = 13.37 ether; + + IERC20 tokenReturnsFalse = IERC20(makeAddr("returns false on transfer")); + + vm.mockCall(address(tokenReturnsFalse), abi.encodeCall(IERC20.transfer, (recipient, amount)), abi.encode(false)); + + vm.expectRevert("GPv2: failed transfer"); + executor.transfer(tokenReturnsFalse, recipient, amount); + } + + function test_reverts_when_too_much_data_is_returned() public { + uint256 amount = 1 ether; + + IERC20 tokenReturnsTooMuchData = IERC20(makeAddr("returns too much data transfer")); + + vm.mockCall( + address(tokenReturnsTooMuchData), + abi.encodeCall(IERC20.transfer, (recipient, amount)), + abi.encodePacked(new bytes(256)) + ); + + vm.expectRevert("GPv2: malformed transfer result"); + executor.transfer(tokenReturnsTooMuchData, recipient, amount); + } + + function test_coerces_invalid_abi_encoded_bool() public { + uint256 amount = 1 ether; + + IERC20 tokenReturnsLargeUint = IERC20(makeAddr("returns uint256 larger than 1")); + + vm.mockCall( + address(tokenReturnsLargeUint), abi.encodeCall(IERC20.transfer, (recipient, amount)), abi.encode(42) + ); + + executor.transfer(tokenReturnsLargeUint, recipient, amount); + } +} diff --git a/test/GPv2SafeERC20/TransferFrom.sol b/test/GPv2SafeERC20/TransferFrom.sol new file mode 100644 index 00000000..a63b71cc --- /dev/null +++ b/test/GPv2SafeERC20/TransferFrom.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +pragma solidity ^0.8.0; + +import {IERC20} from "src/contracts/interfaces/IERC20.sol"; + +import {Helper} from "./Helper.sol"; + +contract TransferFrom is Helper { + address sender = makeAddr("TransferFrom: transfer sender"); + + function test_succeeds_when_internal_call_succeeds() public { + uint256 amount = 13.37 ether; + + IERC20 standardToken = IERC20(makeAddr("standard token")); + vm.mockCall( + address(standardToken), abi.encodeCall(IERC20.transferFrom, (sender, recipient, amount)), abi.encode(true) + ); + + executor.transferFrom(standardToken, sender, recipient, amount); + } + + function test_reverts_on_failed_internal_call() public { + uint256 amount = 42 ether; + + IERC20 revertingToken = IERC20(makeAddr("reverting token")); + + vm.mockCallRevert( + address(revertingToken), + abi.encodeCall(IERC20.transferFrom, (sender, recipient, amount)), + bytes("test error") + ); + + vm.expectRevert("test error"); + executor.transferFrom(revertingToken, sender, recipient, amount); + } + + function test_reverts_when_calling_a_non_contract() public { + uint256 amount = 4.2 ether; + + IERC20 standardToken = IERC20(makeAddr("address without code")); + + vm.expectRevert("GPv2: not a contract"); + executor.transferFrom(standardToken, sender, recipient, amount); + } + + function test_does_not_revert_when_the_internal_call_has_no_return_data() public { + uint256 amount = 13.37 ether; + + IERC20 tokenNoReturnValue = IERC20(makeAddr("does not return on transfer")); + + vm.mockCall( + address(tokenNoReturnValue), abi.encodeCall(IERC20.transferFrom, (sender, recipient, amount)), hex"" + ); + + executor.transferFrom(tokenNoReturnValue, sender, recipient, amount); + } + + function test_reverts_when_the_internal_call_returns_false() public { + uint256 amount = 13.37 ether; + + IERC20 tokenReturnsFalse = IERC20(makeAddr("returns false on transfer")); + + vm.mockCall( + address(tokenReturnsFalse), + abi.encodeCall(IERC20.transferFrom, (sender, recipient, amount)), + abi.encode(false) + ); + + vm.expectRevert("GPv2: failed transferFrom"); + executor.transferFrom(tokenReturnsFalse, sender, recipient, amount); + } + + function test_reverts_when_too_much_data_is_returned() public { + uint256 amount = 1 ether; + + IERC20 tokenReturnsTooMuchData = IERC20(makeAddr("returns too much data transfer")); + + vm.mockCall( + address(tokenReturnsTooMuchData), + abi.encodeCall(IERC20.transferFrom, (sender, recipient, amount)), + abi.encodePacked(new bytes(256)) + ); + + vm.expectRevert("GPv2: malformed transfer result"); + executor.transferFrom(tokenReturnsTooMuchData, sender, recipient, amount); + } + + function test_coerces_invalid_abi_encoded_bool() public { + uint256 amount = 1 ether; + + IERC20 tokenReturnsLargeUint = IERC20(makeAddr("returns uint256 larger than 1")); + + vm.mockCall( + address(tokenReturnsLargeUint), + abi.encodeCall(IERC20.transferFrom, (sender, recipient, amount)), + abi.encode(42) + ); + + executor.transferFrom(tokenReturnsLargeUint, sender, recipient, amount); + } +}