// SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.15; import {MockERC20} from "./utils/mocks/MockERC20.sol"; import {RevertingToken} from "./utils/weird-tokens/RevertingToken.sol"; import {ReturnsTwoToken} from "./utils/weird-tokens/ReturnsTwoToken.sol"; import {ReturnsFalseToken} from "./utils/weird-tokens/ReturnsFalseToken.sol"; import {MissingReturnToken} from "./utils/weird-tokens/MissingReturnToken.sol"; import {ReturnsTooMuchToken} from "./utils/weird-tokens/ReturnsTooMuchToken.sol"; import {ReturnsGarbageToken} from "./utils/weird-tokens/ReturnsGarbageToken.sol"; import {ReturnsTooLittleToken} from "./utils/weird-tokens/ReturnsTooLittleToken.sol"; import {DSTestPlus} from "./utils/DSTestPlus.sol"; import {ERC20} from "../tokens/ERC20.sol"; import {SafeTransferLib} from "../utils/SafeTransferLib.sol"; contract SafeTransferLibTest is DSTestPlus { RevertingToken reverting; ReturnsTwoToken returnsTwo; ReturnsFalseToken returnsFalse; MissingReturnToken missingReturn; ReturnsTooMuchToken returnsTooMuch; ReturnsGarbageToken returnsGarbage; ReturnsTooLittleToken returnsTooLittle; MockERC20 erc20; function setUp() public { reverting = new RevertingToken(); returnsTwo = new ReturnsTwoToken(); returnsFalse = new ReturnsFalseToken(); missingReturn = new MissingReturnToken(); returnsTooMuch = new ReturnsTooMuchToken(); returnsGarbage = new ReturnsGarbageToken(); returnsTooLittle = new ReturnsTooLittleToken(); erc20 = new MockERC20("StandardToken", "ST", 18); erc20.mint(address(this), type(uint256).max); } function testTransferWithMissingReturn() public { verifySafeTransfer(address(missingReturn), address(0xBEEF), 1e18); } function testTransferWithStandardERC20() public { verifySafeTransfer(address(erc20), address(0xBEEF), 1e18); } function testTransferWithReturnsTooMuch() public { verifySafeTransfer(address(returnsTooMuch), address(0xBEEF), 1e18); } function testTransferWithNonContract() public { SafeTransferLib.safeTransfer(ERC20(address(0xBADBEEF)), address(0xBEEF), 1e18); } function testTransferFromWithMissingReturn() public { verifySafeTransferFrom(address(missingReturn), address(0xFEED), address(0xBEEF), 1e18); } function testTransferFromWithStandardERC20() public { verifySafeTransferFrom(address(erc20), address(0xFEED), address(0xBEEF), 1e18); } function testTransferFromWithReturnsTooMuch() public { verifySafeTransferFrom(address(returnsTooMuch), address(0xFEED), address(0xBEEF), 1e18); } function testTransferFromWithNonContract() public { SafeTransferLib.safeTransferFrom(ERC20(address(0xBADBEEF)), address(0xFEED), address(0xBEEF), 1e18); } function testApproveWithMissingReturn() public { verifySafeApprove(address(missingReturn), address(0xBEEF), 1e18); } function testApproveWithStandardERC20() public { verifySafeApprove(address(erc20), address(0xBEEF), 1e18); } function testApproveWithReturnsTooMuch() public { verifySafeApprove(address(returnsTooMuch), address(0xBEEF), 1e18); } function testApproveWithNonContract() public { SafeTransferLib.safeApprove(ERC20(address(0xBADBEEF)), address(0xBEEF), 1e18); } function testTransferETH() public { SafeTransferLib.safeTransferETH(address(0xBEEF), 1e18); } function testFailTransferWithReturnsFalse() public { verifySafeTransfer(address(returnsFalse), address(0xBEEF), 1e18); } function testFailTransferWithReverting() public { verifySafeTransfer(address(reverting), address(0xBEEF), 1e18); } function testFailTransferWithReturnsTooLittle() public { verifySafeTransfer(address(returnsTooLittle), address(0xBEEF), 1e18); } function testFailTransferFromWithReturnsFalse() public { verifySafeTransferFrom(address(returnsFalse), address(0xFEED), address(0xBEEF), 1e18); } function testFailTransferFromWithReverting() public { verifySafeTransferFrom(address(reverting), address(0xFEED), address(0xBEEF), 1e18); } function testFailTransferFromWithReturnsTooLittle() public { verifySafeTransferFrom(address(returnsTooLittle), address(0xFEED), address(0xBEEF), 1e18); } function testFailApproveWithReturnsFalse() public { verifySafeApprove(address(returnsFalse), address(0xBEEF), 1e18); } function testFailApproveWithReverting() public { verifySafeApprove(address(reverting), address(0xBEEF), 1e18); } function testFailApproveWithReturnsTooLittle() public { verifySafeApprove(address(returnsTooLittle), address(0xBEEF), 1e18); } function testTransferWithMissingReturn( address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeTransfer(address(missingReturn), to, amount); } function testTransferWithStandardERC20( address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeTransfer(address(erc20), to, amount); } function testTransferWithReturnsTooMuch( address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeTransfer(address(returnsTooMuch), to, amount); } function testTransferWithGarbage( address to, uint256 amount, bytes memory garbage, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { if ( (garbage.length < 32 || (garbage[0] != 0 || garbage[1] != 0 || garbage[2] != 0 || garbage[3] != 0 || garbage[4] != 0 || garbage[5] != 0 || garbage[6] != 0 || garbage[7] != 0 || garbage[8] != 0 || garbage[9] != 0 || garbage[10] != 0 || garbage[11] != 0 || garbage[12] != 0 || garbage[13] != 0 || garbage[14] != 0 || garbage[15] != 0 || garbage[16] != 0 || garbage[17] != 0 || garbage[18] != 0 || garbage[19] != 0 || garbage[20] != 0 || garbage[21] != 0 || garbage[22] != 0 || garbage[23] != 0 || garbage[24] != 0 || garbage[25] != 0 || garbage[26] != 0 || garbage[27] != 0 || garbage[28] != 0 || garbage[29] != 0 || garbage[30] != 0 || garbage[31] != bytes1(0x01))) && garbage.length != 0 ) return; returnsGarbage.setGarbage(garbage); verifySafeTransfer(address(returnsGarbage), to, amount); } function testTransferWithNonContract( address nonContract, address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { if (uint256(uint160(nonContract)) <= 18 || nonContract.code.length > 0) return; SafeTransferLib.safeTransfer(ERC20(nonContract), to, amount); } function testFailTransferETHToContractWithoutFallback() public { SafeTransferLib.safeTransferETH(address(this), 1e18); } function testTransferFromWithMissingReturn( address from, address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeTransferFrom(address(missingReturn), from, to, amount); } function testTransferFromWithStandardERC20( address from, address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeTransferFrom(address(erc20), from, to, amount); } function testTransferFromWithReturnsTooMuch( address from, address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeTransferFrom(address(returnsTooMuch), from, to, amount); } function testTransferFromWithGarbage( address from, address to, uint256 amount, bytes memory garbage, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { if ( (garbage.length < 32 || (garbage[0] != 0 || garbage[1] != 0 || garbage[2] != 0 || garbage[3] != 0 || garbage[4] != 0 || garbage[5] != 0 || garbage[6] != 0 || garbage[7] != 0 || garbage[8] != 0 || garbage[9] != 0 || garbage[10] != 0 || garbage[11] != 0 || garbage[12] != 0 || garbage[13] != 0 || garbage[14] != 0 || garbage[15] != 0 || garbage[16] != 0 || garbage[17] != 0 || garbage[18] != 0 || garbage[19] != 0 || garbage[20] != 0 || garbage[21] != 0 || garbage[22] != 0 || garbage[23] != 0 || garbage[24] != 0 || garbage[25] != 0 || garbage[26] != 0 || garbage[27] != 0 || garbage[28] != 0 || garbage[29] != 0 || garbage[30] != 0 || garbage[31] != bytes1(0x01))) && garbage.length != 0 ) return; returnsGarbage.setGarbage(garbage); verifySafeTransferFrom(address(returnsGarbage), from, to, amount); } function testTransferFromWithNonContract( address nonContract, address from, address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { if (uint256(uint160(nonContract)) <= 18 || nonContract.code.length > 0) return; SafeTransferLib.safeTransferFrom(ERC20(nonContract), from, to, amount); } function testApproveWithMissingReturn( address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeApprove(address(missingReturn), to, amount); } function testApproveWithStandardERC20( address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeApprove(address(erc20), to, amount); } function testApproveWithReturnsTooMuch( address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeApprove(address(returnsTooMuch), to, amount); } function testApproveWithGarbage( address to, uint256 amount, bytes memory garbage, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { if ( (garbage.length < 32 || (garbage[0] != 0 || garbage[1] != 0 || garbage[2] != 0 || garbage[3] != 0 || garbage[4] != 0 || garbage[5] != 0 || garbage[6] != 0 || garbage[7] != 0 || garbage[8] != 0 || garbage[9] != 0 || garbage[10] != 0 || garbage[11] != 0 || garbage[12] != 0 || garbage[13] != 0 || garbage[14] != 0 || garbage[15] != 0 || garbage[16] != 0 || garbage[17] != 0 || garbage[18] != 0 || garbage[19] != 0 || garbage[20] != 0 || garbage[21] != 0 || garbage[22] != 0 || garbage[23] != 0 || garbage[24] != 0 || garbage[25] != 0 || garbage[26] != 0 || garbage[27] != 0 || garbage[28] != 0 || garbage[29] != 0 || garbage[30] != 0 || garbage[31] != bytes1(0x01))) && garbage.length != 0 ) return; returnsGarbage.setGarbage(garbage); verifySafeApprove(address(returnsGarbage), to, amount); } function testApproveWithNonContract( address nonContract, address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { if (uint256(uint160(nonContract)) <= 18 || nonContract.code.length > 0) return; SafeTransferLib.safeApprove(ERC20(nonContract), to, amount); } function testTransferETH( address recipient, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { // Transferring to msg.sender can fail because it's possible to overflow their ETH balance as it begins non-zero. if (recipient.code.length > 0 || uint256(uint160(recipient)) <= 18 || recipient == msg.sender) return; amount = bound(amount, 0, address(this).balance); SafeTransferLib.safeTransferETH(recipient, amount); } function testFailTransferWithReturnsFalse( address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeTransfer(address(returnsFalse), to, amount); } function testFailTransferWithReverting( address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeTransfer(address(reverting), to, amount); } function testFailTransferWithReturnsTooLittle( address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeTransfer(address(returnsTooLittle), to, amount); } function testFailTransferWithReturnsTwo( address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeTransfer(address(returnsTwo), to, amount); } function testFailTransferWithGarbage( address to, uint256 amount, bytes memory garbage, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { require(garbage.length != 0 && (garbage.length < 32 || garbage[31] != bytes1(0x01))); returnsGarbage.setGarbage(garbage); verifySafeTransfer(address(returnsGarbage), to, amount); } function testFailTransferFromWithReturnsFalse( address from, address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeTransferFrom(address(returnsFalse), from, to, amount); } function testFailTransferFromWithReverting( address from, address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeTransferFrom(address(reverting), from, to, amount); } function testFailTransferFromWithReturnsTooLittle( address from, address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeTransferFrom(address(returnsTooLittle), from, to, amount); } function testFailTransferFromWithReturnsTwo( address from, address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeTransferFrom(address(returnsTwo), from, to, amount); } function testFailTransferFromWithGarbage( address from, address to, uint256 amount, bytes memory garbage, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { require(garbage.length != 0 && (garbage.length < 32 || garbage[31] != bytes1(0x01))); returnsGarbage.setGarbage(garbage); verifySafeTransferFrom(address(returnsGarbage), from, to, amount); } function testFailApproveWithReturnsFalse( address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeApprove(address(returnsFalse), to, amount); } function testFailApproveWithReverting( address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeApprove(address(reverting), to, amount); } function testFailApproveWithReturnsTooLittle( address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeApprove(address(returnsTooLittle), to, amount); } function testFailApproveWithReturnsTwo( address to, uint256 amount, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { verifySafeApprove(address(returnsTwo), to, amount); } function testFailApproveWithGarbage( address to, uint256 amount, bytes memory garbage, bytes calldata brutalizeWith ) public brutalizeMemory(brutalizeWith) { require(garbage.length != 0 && (garbage.length < 32 || garbage[31] != bytes1(0x01))); returnsGarbage.setGarbage(garbage); verifySafeApprove(address(returnsGarbage), to, amount); } function testFailTransferETHToContractWithoutFallback(uint256 amount, bytes calldata brutalizeWith) public brutalizeMemory(brutalizeWith) { SafeTransferLib.safeTransferETH(address(this), amount); } function verifySafeTransfer( address token, address to, uint256 amount ) internal { uint256 preBal = ERC20(token).balanceOf(to); SafeTransferLib.safeTransfer(ERC20(address(token)), to, amount); uint256 postBal = ERC20(token).balanceOf(to); if (to == address(this)) { assertEq(preBal, postBal); } else { assertEq(postBal - preBal, amount); } } function verifySafeTransferFrom( address token, address from, address to, uint256 amount ) internal { forceApprove(token, from, address(this), amount); // We cast to MissingReturnToken here because it won't check // that there was return data, which accommodates all tokens. MissingReturnToken(token).transfer(from, amount); uint256 preBal = ERC20(token).balanceOf(to); SafeTransferLib.safeTransferFrom(ERC20(token), from, to, amount); uint256 postBal = ERC20(token).balanceOf(to); if (from == to) { assertEq(preBal, postBal); } else { assertEq(postBal - preBal, amount); } } function verifySafeApprove( address token, address to, uint256 amount ) internal { SafeTransferLib.safeApprove(ERC20(address(token)), to, amount); assertEq(ERC20(token).allowance(address(this), to), amount); } function forceApprove( address token, address from, address to, uint256 amount ) internal { uint256 slot = token == address(erc20) ? 4 : 2; // Standard ERC20 name and symbol aren't constant. hevm.store( token, keccak256(abi.encode(to, keccak256(abi.encode(from, uint256(slot))))), bytes32(uint256(amount)) ); assertEq(ERC20(token).allowance(from, to), amount, "wrong allowance"); } }