diff --git a/README.md b/README.md index 51ab985..d0f6d23 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,16 @@ It also contains generic contract interfaces (for EIP/ERC) that can be used. ## Current contracts -| Name | Description | Type | Latest version | -| ---------------- | ----------------------------------------------------------------------------------------------------------- | -------- | -------------- | -| OwnableTwoSteps | Contract for managing ownership of a smart contract. The transfer of ownership is done in a 2-step process. | Contract | 1.0.0 | -| SignatureChecker | Contract for verifying the validity of a signature for EOA (64-byte, 65-byte signatures) and EIP-1271. | Contract | 1.0.0 | -| ReentrancyGuard | Contract with a modifier to prevent reentrancy calls. | Contract | 1.0.0 | +| Name | Description | Type | Latest version | +| ---------------- | ----------------------------------------------------------------------------------------------------------------------------- | -------- | -------------- | +| OwnableTwoSteps | Contract for managing ownership of a smart contract. The transfer of ownership is done in a 2-step process. | Contract | 2.0.0 | +| SignatureChecker | Contract for verifying the validity of a signature for EOA (64-byte, 65-byte signatures) and EIP-1271. | Contract | 2.0.0 | +| ReentrancyGuard | Contract with a modifier to prevent reentrancy calls. | Contract | 2.0.0 | +| LowLevelETH | Low-level call functions to transfer ETH or return ETH back to sender in a payable function | Contract | 2.0.0 | +| LowLevelWETH | Low-level call functions to transfer ETH with an option to wrap to WETH if the original ETH transfer fails within a gas limit | Contract | 2.0.0 | +| LowLevelERC20 | Low-level call functions for ERC20 functions | Contract | 2.0.0 | +| LowLevelERC721 | Low-level call functions for ERC721 functions | Contract | 2.0.0 | +| LowLevelERC1155 | Low-level call functions for ERC1155 functions | Contract | 2.0.0 | ## About this repo diff --git a/package.json b/package.json index e455c95..62fa117 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "files": [ "/abis/*.json", "/contracts/*.sol", - "/contracts/interfaces/*.sol" + "/contracts/interfaces/*.sol", + "/contracts/interfaces/generic/*.sol", + "/contracts/lowLevelCallers/*.sol" ], "keywords": [ "looksrare", @@ -75,6 +77,7 @@ "release-it": "^15.0.0", "solhint": "^3.3.7", "solidity-coverage": "^0.7.21", + "solmate": "^6.6.1", "ts-node": "^10.1.0", "typechain": "^5.1.2", "typescript": "^4.5.2" diff --git a/test/foundry/LowLevelERC1155.t.sol b/test/foundry/LowLevelERC1155.t.sol new file mode 100644 index 0000000..90d53e4 --- /dev/null +++ b/test/foundry/LowLevelERC1155.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {LowLevelERC1155} from "../../contracts/lowLevelCallers/LowLevelERC1155.sol"; +import {MockERC1155} from "../mock/MockERC1155.sol"; +import {TestHelpers} from "./utils/TestHelpers.sol"; + +contract ImplementedLowLevelERC1155 is LowLevelERC1155 { + function safeTransferFromERC1155( + address collection, + address from, + address to, + uint256 tokenId, + uint256 amount + ) external { + _executeERC1155SafeTransferFrom(collection, from, to, tokenId, amount); + } + + function safeBatchTransferFromERC1155( + address collection, + address from, + address to, + uint256[] calldata tokenIds, + uint256[] calldata amounts + ) external { + _executeERC1155SafeBatchTransferFrom(collection, from, to, tokenIds, amounts); + } +} + +abstract contract TestParameters { + address internal _sender = address(100); + address internal _recipient = address(101); +} + +contract LowLevelERC1155Test is TestParameters, TestHelpers { + ImplementedLowLevelERC1155 public lowLevelERC1155; + MockERC1155 public mockERC1155; + + function setUp() external { + lowLevelERC1155 = new ImplementedLowLevelERC1155(); + mockERC1155 = new MockERC1155(); + } + + function testSafeTransferFromERC1155(uint256 tokenId, uint256 amount) external asPrankedUser(_sender) { + mockERC1155.mint(_sender, tokenId, amount); + mockERC1155.setApprovalForAll(address(lowLevelERC1155), true); + lowLevelERC1155.safeTransferFromERC1155(address(mockERC1155), _sender, _recipient, tokenId, amount); + assertEq(mockERC1155.balanceOf(_recipient, tokenId), amount); + } + + function testSafeBatchTransferFromERC1155( + uint256 tokenId0, + uint256 amount0, + uint256 amount1 + ) external asPrankedUser(_sender) { + vm.assume(tokenId0 < type(uint256).max); + uint256 tokenId1 = tokenId0 + 1; + mockERC1155.mint(_sender, tokenId0, amount0); + mockERC1155.mint(_sender, tokenId1, amount1); + mockERC1155.setApprovalForAll(address(lowLevelERC1155), true); + + uint256[] memory amounts = new uint256[](2); + amounts[0] = amount0; + amounts[1] = amount1; + + uint256[] memory tokenIds = new uint256[](2); + tokenIds[0] = tokenId0; + tokenIds[1] = tokenId1; + + lowLevelERC1155.safeBatchTransferFromERC1155(address(mockERC1155), _sender, _recipient, tokenIds, amounts); + assertEq(mockERC1155.balanceOf(_recipient, tokenId0), amount0); + assertEq(mockERC1155.balanceOf(_recipient, tokenId1), amount1); + } +} diff --git a/test/foundry/LowLevelERC20.t.sol b/test/foundry/LowLevelERC20.t.sol new file mode 100644 index 0000000..bf7d228 --- /dev/null +++ b/test/foundry/LowLevelERC20.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {LowLevelERC20} from "../../contracts/lowLevelCallers/LowLevelERC20.sol"; +import {MockERC20} from "../mock/MockERC20.sol"; +import {TestHelpers} from "./utils/TestHelpers.sol"; + +contract ImplementedLowLevelERC20 is LowLevelERC20 { + function transferERC20( + address currency, + address to, + uint256 amount + ) external { + _executeERC20DirectTransfer(currency, to, amount); + } + + function transferFromERC20( + address currency, + address from, + address to, + uint256 amount + ) external { + _executeERC20TransferFrom(currency, from, to, amount); + } +} + +abstract contract TestParameters { + address internal _sender = address(100); + address internal _recipient = address(101); +} + +contract LowLevelERC20Test is TestHelpers, TestParameters { + ImplementedLowLevelERC20 public lowLevelERC20; + MockERC20 public mockERC20; + + function setUp() external { + lowLevelERC20 = new ImplementedLowLevelERC20(); + mockERC20 = new MockERC20(); + } + + function testTransferFromERC20(uint256 amount) external asPrankedUser(_sender) { + mockERC20.mint(_sender, amount); + mockERC20.approve(address(lowLevelERC20), amount); + lowLevelERC20.transferFromERC20(address(mockERC20), _sender, _recipient, amount); + assertEq(mockERC20.balanceOf(_recipient), amount); + } + + function testTransferERC20(uint256 amount) external asPrankedUser(_sender) { + mockERC20.mint(address(lowLevelERC20), amount); + lowLevelERC20.transferERC20(address(mockERC20), _recipient, amount); + assertEq(mockERC20.balanceOf(_recipient), amount); + } +} diff --git a/test/foundry/LowLevelERC721.t.sol b/test/foundry/LowLevelERC721.t.sol new file mode 100644 index 0000000..86464e9 --- /dev/null +++ b/test/foundry/LowLevelERC721.t.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {LowLevelERC721} from "../../contracts/lowLevelCallers/LowLevelERC721.sol"; +import {MockERC721} from "../mock/MockERC721.sol"; +import {TestHelpers} from "./utils/TestHelpers.sol"; + +contract ImplementedLowLevelERC721 is LowLevelERC721 { + function transferERC721( + address collection, + address from, + address to, + uint256 tokenId + ) external { + _executeERC721TransferFrom(collection, from, to, tokenId); + } +} + +abstract contract TestParameters { + address internal _sender = address(100); + address internal _recipient = address(101); +} + +contract LowLevelERC721Test is TestParameters, TestHelpers { + ImplementedLowLevelERC721 public lowLevelERC721; + MockERC721 public mockERC721; + + function setUp() external { + lowLevelERC721 = new ImplementedLowLevelERC721(); + mockERC721 = new MockERC721(); + } + + function testTransferFromERC721(uint256 tokenId) external asPrankedUser(_sender) { + mockERC721.mint(_sender, tokenId); + mockERC721.setApprovalForAll(address(lowLevelERC721), true); + lowLevelERC721.transferERC721(address(mockERC721), _sender, _recipient, tokenId); + assertEq(mockERC721.ownerOf(tokenId), _recipient); + } +} diff --git a/test/foundry/LowLevelETH.t.sol b/test/foundry/LowLevelETH.t.sol new file mode 100644 index 0000000..0ae560f --- /dev/null +++ b/test/foundry/LowLevelETH.t.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {LowLevelETH} from "../../contracts/lowLevelCallers/LowLevelETH.sol"; +import {TestHelpers} from "./utils/TestHelpers.sol"; + +contract ImplementedLowLevelETH is LowLevelETH { + function transferETH(address _to) external payable { + _transferETH(_to, msg.value); + } + + function transferETHAndReturnFunds() external payable { + _returnETHIfAny(); + } + + function transferETHAndReturnFundsExceptOneWei() external payable { + _returnETHIfAnyWithOneWeiLeft(); + } +} + +abstract contract TestParameters { + address internal _sender = address(100); + address internal _recipient = address(101); + uint256 internal _GAS_LIMIT = 10000; +} + +contract LowLevelETHTest is TestParameters, TestHelpers { + ImplementedLowLevelETH public lowLevelETH; + + function setUp() external { + lowLevelETH = new ImplementedLowLevelETH(); + } + + function testTransferETH(address randomSender, uint112 amount) external payable { + vm.deal(randomSender, amount); + vm.prank(randomSender); + lowLevelETH.transferETH{value: amount}(_recipient); + assertEq(_recipient.balance, amount); + } + + function testTransferETHAndReturnFunds(uint112 amount) external payable asPrankedUser(_sender) { + vm.deal(_sender, amount); + lowLevelETH.transferETHAndReturnFunds{value: amount}(); + assertEq(_sender.balance, amount); + } + + function testTransferETHAndReturnFundsExceptOneWei(uint112 amount) external payable asPrankedUser(_sender) { + vm.deal(_sender, amount); + lowLevelETH.transferETHAndReturnFundsExceptOneWei{value: amount}(); + + if (amount > 1) { + assertEq(_sender.balance, amount - 1); + assertEq(address(lowLevelETH).balance, 1); + } else { + assertEq(_sender.balance, 0); + assertEq(address(lowLevelETH).balance, amount); + } + } +} diff --git a/test/foundry/LowLevelWETH.t.sol b/test/foundry/LowLevelWETH.t.sol new file mode 100644 index 0000000..8ee062d --- /dev/null +++ b/test/foundry/LowLevelWETH.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {WETH} from "solmate/src/tokens/WETH.sol"; +import {LowLevelWETH} from "../../contracts/lowLevelCallers/LowLevelWETH.sol"; +import {TestHelpers} from "./utils/TestHelpers.sol"; + +contract ImplementedLowLevelWETH is LowLevelWETH { + function transferETH( + address _WETH, + address _to, + uint256 _amount, + uint256 _gasLimit + ) external payable { + _transferETHAndWrapIfFailWithGasLimit(_WETH, _to, _amount, _gasLimit); + } +} + +abstract contract TestParameters { + address internal _sender = address(100); + uint256 internal _GAS_LIMIT = 10000; +} + +contract RecipientFallback { + ImplementedLowLevelWETH public lowLevelWETH; + + receive() external payable { + // Infinite loop + for (uint256 i; i < type(uint256).max; i++) { + keccak256(abi.encode(i)); + } + } +} + +contract LowLevelWETHTest is TestParameters, TestHelpers { + ImplementedLowLevelWETH public lowLevelWETH; + RecipientFallback public recipientFallback; + WETH public weth; + + function setUp() external { + lowLevelWETH = new ImplementedLowLevelWETH(); + recipientFallback = new RecipientFallback(); + weth = new WETH(); + } + + function testTransferETH(uint256 amount) external payable asPrankedUser(_sender) { + vm.deal(_sender, amount); + lowLevelWETH.transferETH{value: amount}(address(weth), address(recipientFallback), amount, _GAS_LIMIT); + assertEq(address(recipientFallback).balance, 0); + assertEq(weth.balanceOf(address(recipientFallback)), amount); + } +} diff --git a/test/foundry/OwnableTwoSteps.t.sol b/test/foundry/OwnableTwoSteps.t.sol index 416d836..c40702a 100644 --- a/test/foundry/OwnableTwoSteps.t.sol +++ b/test/foundry/OwnableTwoSteps.t.sol @@ -6,7 +6,7 @@ import {IOwnableTwoSteps} from "../../contracts/interfaces/IOwnableTwoSteps.sol" import {TestHelpers} from "./utils/TestHelpers.sol"; abstract contract TestParameters { - address internal _OWNER = address(42); + address internal _owner = address(42); uint256 internal _delay = 6 hours; } @@ -19,12 +19,12 @@ contract ImplementedOwnableTwoSteps is OwnableTwoSteps { contract OwnableTwoStepsTest is TestParameters, TestHelpers, IOwnableTwoSteps { ImplementedOwnableTwoSteps public ownableTwoSteps; - function setUp() public asPrankedUser(_OWNER) { + function setUp() public asPrankedUser(_owner) { ownableTwoSteps = new ImplementedOwnableTwoSteps(_delay); } function testConstructor() public { - assertEq(ownableTwoSteps.owner(), _OWNER); + assertEq(ownableTwoSteps.owner(), _owner); assertEq(ownableTwoSteps.potentialOwner(), address(0)); assertEq(uint8(ownableTwoSteps.status()), uint8(Status.NoOngoingTransfer)); assertEq(ownableTwoSteps.delay(), _delay); @@ -34,9 +34,9 @@ contract OwnableTwoStepsTest is TestParameters, TestHelpers, IOwnableTwoSteps { address newOwner = address(45); // 1. Initiate ownership transfer - vm.prank(_OWNER); + vm.prank(_owner); vm.expectEmit(false, false, false, true); - emit InitiateOwnershipTransfer(_OWNER, newOwner); + emit InitiateOwnershipTransfer(_owner, newOwner); ownableTwoSteps.initiateOwnershipTransfer(newOwner); assertEq(ownableTwoSteps.potentialOwner(), newOwner); assertEq(uint8(ownableTwoSteps.status()), uint8(Status.TransferInProgress)); @@ -51,7 +51,7 @@ contract OwnableTwoStepsTest is TestParameters, TestHelpers, IOwnableTwoSteps { assertEq(uint8(ownableTwoSteps.status()), uint8(Status.NoOngoingTransfer)); } - function testRenounceOwnership() public asPrankedUser(_OWNER) { + function testRenounceOwnership() public asPrankedUser(_owner) { // 1. Initiate renouncement of ownership vm.expectEmit(false, false, false, true); emit InitiateOwnershipRenouncement(block.timestamp + _delay); @@ -71,12 +71,12 @@ contract OwnableTwoStepsTest is TestParameters, TestHelpers, IOwnableTwoSteps { assertEq(uint8(ownableTwoSteps.status()), uint8(Status.NoOngoingTransfer)); } - function testCancelTransferOwnership() public asPrankedUser(_OWNER) { + function testCancelTransferOwnership() public asPrankedUser(_owner) { address newOwner = address(45); // 1. Initiate ownership transfer vm.expectEmit(false, false, false, true); - emit InitiateOwnershipTransfer(_OWNER, newOwner); + emit InitiateOwnershipTransfer(_owner, newOwner); ownableTwoSteps.initiateOwnershipTransfer(newOwner); assertEq(ownableTwoSteps.potentialOwner(), newOwner); assertEq(uint8(ownableTwoSteps.status()), uint8(Status.TransferInProgress)); @@ -86,7 +86,7 @@ contract OwnableTwoStepsTest is TestParameters, TestHelpers, IOwnableTwoSteps { emit CancelOwnershipTransfer(); ownableTwoSteps.cancelOwnershipTransfer(); assertEq(ownableTwoSteps.potentialOwner(), address(0)); - assertEq(ownableTwoSteps.owner(), _OWNER); + assertEq(ownableTwoSteps.owner(), _owner); assertEq(uint8(ownableTwoSteps.status()), uint8(Status.NoOngoingTransfer)); // 3. Initiate ownership renouncement @@ -102,7 +102,7 @@ contract OwnableTwoStepsTest is TestParameters, TestHelpers, IOwnableTwoSteps { emit CancelOwnershipTransfer(); ownableTwoSteps.cancelOwnershipTransfer(); assertEq(ownableTwoSteps.potentialOwner(), address(0)); - assertEq(ownableTwoSteps.owner(), _OWNER); + assertEq(ownableTwoSteps.owner(), _owner); assertEq(uint8(ownableTwoSteps.status()), uint8(Status.NoOngoingTransfer)); } @@ -111,9 +111,9 @@ contract OwnableTwoStepsTest is TestParameters, TestHelpers, IOwnableTwoSteps { address wrongOwner = address(30); // 1. Initiate ownership transfer - vm.prank(_OWNER); + vm.prank(_owner); vm.expectEmit(false, false, false, true); - emit InitiateOwnershipTransfer(_OWNER, newOwner); + emit InitiateOwnershipTransfer(_owner, newOwner); ownableTwoSteps.initiateOwnershipTransfer(newOwner); vm.prank(wrongOwner); @@ -121,7 +121,7 @@ contract OwnableTwoStepsTest is TestParameters, TestHelpers, IOwnableTwoSteps { ownableTwoSteps.confirmOwnershipTransfer(); } - function testCannotConfirmRenouncementOwnershipPriorToTimelock() public asPrankedUser(_OWNER) { + function testCannotConfirmRenouncementOwnershipPriorToTimelock() public asPrankedUser(_owner) { // Initiate renouncement of ownership vm.expectEmit(false, false, false, true); emit InitiateOwnershipRenouncement(block.timestamp + _delay); @@ -137,7 +137,7 @@ contract OwnableTwoStepsTest is TestParameters, TestHelpers, IOwnableTwoSteps { } function testOwnableFunctionsOnlyCallableByOwner(address randomUser) public asPrankedUser(randomUser) { - vm.assume(randomUser != _OWNER); + vm.assume(randomUser != _owner); vm.expectRevert(NotOwner.selector); ownableTwoSteps.cancelOwnershipTransfer(); @@ -152,7 +152,7 @@ contract OwnableTwoStepsTest is TestParameters, TestHelpers, IOwnableTwoSteps { ownableTwoSteps.initiateOwnershipRenouncement(); } - function testCannotConfirmOwnershipOrRenouncementTransferIfNotInProgress() public asPrankedUser(_OWNER) { + function testCannotConfirmOwnershipOrRenouncementTransferIfNotInProgress() public asPrankedUser(_owner) { vm.expectRevert(TransferNotInProgress.selector); ownableTwoSteps.confirmOwnershipTransfer(); @@ -160,12 +160,12 @@ contract OwnableTwoStepsTest is TestParameters, TestHelpers, IOwnableTwoSteps { ownableTwoSteps.confirmOwnershipRenouncement(); } - function testCannotCancelTransferIfNoTransferInProgress() public asPrankedUser(_OWNER) { + function testCannotCancelTransferIfNoTransferInProgress() public asPrankedUser(_owner) { vm.expectRevert(NoOngoingTransferInProgress.selector); ownableTwoSteps.cancelOwnershipTransfer(); } - function testCannotRenounceOrInitiateTransferASecondtime() public asPrankedUser(_OWNER) { + function testCannotRenounceOrInitiateTransferASecondtime() public asPrankedUser(_owner) { // 1. Cannot initiate renouncement/transfer to new owner after transfer to new owner is initiated address newOwner = address(45); ownableTwoSteps.initiateOwnershipTransfer(newOwner); diff --git a/test/mock/MockERC1155.sol b/test/mock/MockERC1155.sol new file mode 100644 index 0000000..ed508b0 --- /dev/null +++ b/test/mock/MockERC1155.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.7; + +import {ERC1155} from "solmate/src/tokens/ERC1155.sol"; + +contract MockERC1155 is ERC1155 { + function uri(uint256) public pure override returns (string memory) { + return "uri"; + } + + function mint( + address to, + uint256 tokenId, + uint256 amount + ) public { + _mint(to, tokenId, amount, ""); + } +} diff --git a/test/mock/MockERC20.sol b/test/mock/MockERC20.sol new file mode 100644 index 0000000..019bed0 --- /dev/null +++ b/test/mock/MockERC20.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.7; + +import {ERC20} from "solmate/src/tokens/ERC20.sol"; + +contract MockERC20 is ERC20("MockERC20", "MockERC20", 18) { + function mint(address to, uint256 amount) public { + _mint(to, amount); + } +} diff --git a/test/mock/MockERC721.sol b/test/mock/MockERC721.sol new file mode 100644 index 0000000..079090e --- /dev/null +++ b/test/mock/MockERC721.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.7; + +import {ERC721} from "solmate/src/tokens/ERC721.sol"; +import {IERC165} from "../../contracts/interfaces/generic/IERC165.sol"; + +contract MockERC721 is ERC721("MockERC721", "MockERC721") { + function mint(address to, uint256 tokenId) public { + _mint(to, tokenId); + } + + function batchMint(address to, uint256[] memory tokenIds) public { + for (uint256 i; i < tokenIds.length; i++) { + _mint(to, tokenIds[i]); + } + } + + function tokenURI(uint256) public pure override returns (string memory) { + return "tokenURI"; + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return super.supportsInterface(interfaceId); + } +} diff --git a/yarn.lock b/yarn.lock index 8b85137..8b8e013 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10329,6 +10329,11 @@ solidity-coverage@^0.7.21: shelljs "^0.8.3" web3-utils "^1.3.0" +solmate@^6.6.1: + version "6.6.1" + resolved "https://registry.yarnpkg.com/solmate/-/solmate-6.6.1.tgz#f0907e1cdada6dd5fbfe11811ab545162b713197" + integrity sha512-WHvRXQvGtgR6R9nmkDTz/d+oULMqf/D33rlzQyadTX2SbuTmaW7ToEjGjGtWUVCQwZsZ/JP3vbOEVv7fB50btg== + source-map-resolve@^0.5.0: version "0.5.3" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"