From 066ea124c6451503b40264d7bbe2448fb63da2d9 Mon Sep 17 00:00:00 2001 From: Gabriel Cartier <4060669+GabrielCartier@users.noreply.github.com> Date: Wed, 10 Jul 2024 13:33:20 -0400 Subject: [PATCH 1/6] added testing script --- script/TestPacking.s.sol | 4 +-- script/offer/CancelTestOffer.s.sol | 20 ++++++++++++ script/offer/CreateTestOffer.s.sol | 52 ++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 script/offer/CancelTestOffer.s.sol create mode 100644 script/offer/CreateTestOffer.s.sol diff --git a/script/TestPacking.s.sol b/script/TestPacking.s.sol index 6d75667..18487bf 100644 --- a/script/TestPacking.s.sol +++ b/script/TestPacking.s.sol @@ -8,9 +8,9 @@ contract TestPacking is Script { // Exclude from coverage report function test() public {} - function run() external { + function run() external view { OfferItem[] memory offerItems = new OfferItem[](1); - offerItems[0] = OfferItem({tokenAddress: 0x7DA16cd402106Adaf39092215DbB54092b80B6E6, tokenId: 2}); + offerItems[0] = OfferItem({tokenAddress: 0x7DA16cd402106Adaf39092215DbB54092b80B6E6, tokenId: 2, amount: 0}); Offer memory offer = Offer({ sender: 0x7DA16cd402106Adaf39092215DbB54092b80B6E6, diff --git a/script/offer/CancelTestOffer.s.sol b/script/offer/CancelTestOffer.s.sol new file mode 100644 index 0000000..87ac9d8 --- /dev/null +++ b/script/offer/CancelTestOffer.s.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.18; + +import "forge-std/Script.sol"; +import "../../src/EchoBlast.sol"; + +contract CancelTestOffer is Script { + // Exclude from coverage report + function test() public {} + + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + bytes32 offerId = 0x16CBC4D75FA829A5524573F169FCAC91FC8935F41F58B705F0B9E624FEC0A9CB; + EchoBlast echo = EchoBlast(0xF37c2C531a6ffEBb8d3EdCF34e54b0E26047dA4C); + echo.cancelOffer(offerId); + + vm.stopBroadcast(); + } +} diff --git a/script/offer/CreateTestOffer.s.sol b/script/offer/CreateTestOffer.s.sol new file mode 100644 index 0000000..57a0901 --- /dev/null +++ b/script/offer/CreateTestOffer.s.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.18; + +import "forge-std/Script.sol"; +import "../../src/EchoBlast.sol"; +import "../../test/utils/OfferUtils.sol"; + +contract CreateTestOffer is Script, OfferUtils { + // Exclude from coverage report + function test() public override {} + + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + EchoBlast echo = EchoBlast(0xF37c2C531a6ffEBb8d3EdCF34e54b0E26047dA4C); + + address sender = address(0x213bE2f484Ab480db4f18b0Fe4C38e1C25877f09); + address receiver = address(0x20F039821DE7Db6f543c7C07D419800Eb9Bd01Af); + address NFT = address(0x43bE93945E168A205D708F1A41A124fA302e1f76); + uint256 expiration = 1818917576; + address[] memory senderTokenAddresses = new address[](1); + senderTokenAddresses[0] = NFT; + uint256[] memory senderTokenIds = new uint256[](1); + senderTokenIds[0] = 2; + uint256[] memory senderTokenAmounts = new uint256[](1); + senderTokenAmounts[0] = 0; + address[] memory receiverTokenAddresses = new address[](1); + receiverTokenAddresses[0] = NFT; + uint256[] memory receiverTokenIds = new uint256[](1); + receiverTokenIds[0] = 1; + uint256[] memory receiverTokenAmounts = new uint256[](1); + receiverTokenAmounts[0] = 0; + + Offer memory offer = generateOffer( + sender, + senderTokenAddresses, + senderTokenIds, + senderTokenAmounts, + block.chainid, + receiver, + receiverTokenAddresses, + receiverTokenIds, + receiverTokenAmounts, + block.chainid, + expiration, + OfferState.OPEN + ); + echo.createOffer(offer); + + vm.stopBroadcast(); + } +} From 8b3b1f6711f6aa8c123ed014a8829cb4f8261f98 Mon Sep 17 00:00:00 2001 From: Gabriel Cartier <4060669+GabrielCartier@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:31:20 -0400 Subject: [PATCH 2/6] Added ERC20 support added tests # Conflicts: # test/EdgeCasesTest.t.sol # test/utils/OfferUtils.sol --- script/offer/CreateTestOffer.s.sol | 20 ++--- src/Echo.sol | 2 +- src/escrow/EscrowHandler.sol | 19 +++- src/types/OfferItem.sol | 6 +- test/AcceptOffer.t.sol | 64 ++++++++++--- test/Approval.t.sol | 126 +++++++++++++++++++++----- test/BaseTest.t.sol | 138 ++++++++++++++++++++++++----- test/CancelOffer.t.sol | 44 ++++++--- test/CreateOffer.t.sol | 97 ++++++++++++-------- test/EdgeCasesTest.t.sol | 81 +++++++++++++++++ test/ExecuteOffer.t.sol | 39 +++++--- test/Expiration.t.sol | 23 +++-- test/Ownership.t.sol | 22 +++-- test/Paused.t.sol | 44 +++++---- test/RedeemOffer.t.sol | 30 +++++++ test/WrongAssets.t.sol | 88 +++++++++--------- test/utils/OfferUtils.sol | 61 +++++++------ 17 files changed, 641 insertions(+), 263 deletions(-) create mode 100644 test/EdgeCasesTest.t.sol diff --git a/script/offer/CreateTestOffer.s.sol b/script/offer/CreateTestOffer.s.sol index 57a0901..8a5db29 100644 --- a/script/offer/CreateTestOffer.s.sol +++ b/script/offer/CreateTestOffer.s.sol @@ -31,20 +31,12 @@ contract CreateTestOffer is Script, OfferUtils { uint256[] memory receiverTokenAmounts = new uint256[](1); receiverTokenAmounts[0] = 0; - Offer memory offer = generateOffer( - sender, - senderTokenAddresses, - senderTokenIds, - senderTokenAmounts, - block.chainid, - receiver, - receiverTokenAddresses, - receiverTokenIds, - receiverTokenAmounts, - block.chainid, - expiration, - OfferState.OPEN - ); + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); + + Offer memory offer = generateOffer(sender, senderItems, receiver, receiverItems, expiration, OfferState.OPEN); echo.createOffer(offer); vm.stopBroadcast(); diff --git a/src/Echo.sol b/src/Echo.sol index e7f8469..83afb08 100644 --- a/src/Echo.sol +++ b/src/Echo.sol @@ -44,7 +44,7 @@ contract Echo is ReentrancyGuard, Admin, Banker, Escrow, EchoState { } Offer memory offer = offers[offerId]; - // @dev We dont do a check on whether the offer exsits or not because + // @dev We dont do a check on whether the offer exists or not because // if it doesn't exist offer.receiver = address(0) which can't be msg.sender if (offer.receiver != msg.sender) { revert InvalidReceiver(); diff --git a/src/escrow/EscrowHandler.sol b/src/escrow/EscrowHandler.sol index 5fbe750..565ef70 100644 --- a/src/escrow/EscrowHandler.sol +++ b/src/escrow/EscrowHandler.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.18; import "solmate/tokens/ERC721.sol"; +import "solady/src/tokens/ERC20.sol"; import "../types/OfferItems.sol"; abstract contract EscrowHandler is ERC721TokenReceiver { @@ -10,11 +11,25 @@ abstract contract EscrowHandler is ERC721TokenReceiver { collection.safeTransferFrom(from, to, id); } - // @dev function to transfer items from an offer type + function _transferERC20(address tokenAddress, uint256 amount, address from, address to) internal { + ERC20 token = ERC20(tokenAddress); + if (address(this) == from) { + token.transfer(to, amount); + } else { + token.transferFrom(from, to, amount); + } + } + + // @dev function to transfer items from an offer type. We check for the amount to distinguish ERC20 function _transferOfferItems(OfferItems memory offerItems, address from, address to) internal { uint256 length = offerItems.items.length; for (uint256 i = 0; i < length;) { - _transferERC721(offerItems.items[i].tokenAddress, offerItems.items[i].tokenId, from, to); + OfferItem memory item = offerItems.items[i]; + if (item.amount >= 1) { + _transferERC20(item.tokenAddress, item.amount, from, to); + } else { + _transferERC721(item.tokenAddress, item.tokenId, from, to); + } unchecked { i++; } diff --git a/src/types/OfferItem.sol b/src/types/OfferItem.sol index f90ad9b..70e5feb 100644 --- a/src/types/OfferItem.sol +++ b/src/types/OfferItem.sol @@ -1,9 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.18; -// @dev Struct representing an offer item (a token) -// @dev We only support ERC721 on EVM chains for now +// @dev Struct representing an offer item +// @dev We support ERC721 and ERC20 +// @dev If amount is 0, then we use tokenId. struct OfferItem { address tokenAddress; uint256 tokenId; + uint256 amount; } diff --git a/test/AcceptOffer.t.sol b/test/AcceptOffer.t.sol index 5ea30b7..43b1075 100644 --- a/test/AcceptOffer.t.sol +++ b/test/AcceptOffer.t.sol @@ -12,24 +12,24 @@ contract AcceptOfferTest is BaseTest { senderTokenAddresses[0] = apeAddress; uint256[] memory senderTokenIds = new uint256[](1); senderTokenIds[0] = ape2Id; + uint256[] memory senderTokenAmounts = new uint256[](1); + senderTokenAmounts[0] = 0; address[] memory receiverTokenAddresses = new address[](1); receiverTokenAddresses[0] = birdAddress; uint256[] memory receiverTokenIds = new uint256[](1); receiverTokenIds[0] = bird2Id; + uint256[] memory receiverTokenAmounts = new uint256[](1); + receiverTokenAmounts[0] = 0; + + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); + + Offer memory invalidOffer = + generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); - Offer memory invalidOffer = generateOffer( - account1, - senderTokenAddresses, - senderTokenIds, - block.chainid, - account2, - receiverTokenAddresses, - receiverTokenIds, - block.chainid, - in6hours, - OfferState.OPEN - ); bytes32 offerId = generateOfferId(invalidOffer); vm.prank(account2); @@ -114,4 +114,44 @@ contract AcceptOfferTest is BaseTest { // validate that the receiver items are in escrow assertOfferItemsOwnership(offer.receiverItems.items, address(echo)); } + + function testCanAcceptOfferMultipleTokens() public { + Offer memory offer = _createAndAcceptMultiTokensOffer(); + + bytes32 offerId = generateOfferId(offer); + + Offer memory updatedOffer = Offer({ + sender: offer.sender, + receiver: offer.receiver, + senderItems: offer.senderItems, + receiverItems: offer.receiverItems, + expiration: offer.expiration, + state: OfferState.ACCEPTED + }); + + ( + address sender, + address receiver, + OfferItems memory senderItems, + OfferItems memory receiverItems, + uint256 expiration, + OfferState state + ) = echo.offers(offerId); + assertOfferEq( + updatedOffer, + Offer({ + sender: sender, + receiver: receiver, + senderItems: senderItems, + receiverItems: receiverItems, + expiration: expiration, + state: state + }) + ); + + // validate that the sender items are in escrow + assertOfferItemsOwnership(offer.senderItems.items, address(echo)); + // validate that the receiver items are in escrow + assertOfferItemsOwnership(offer.receiverItems.items, address(echo)); + } } diff --git a/test/Approval.t.sol b/test/Approval.t.sol index 88ac270..27fdf38 100644 --- a/test/Approval.t.sol +++ b/test/Approval.t.sol @@ -6,58 +6,84 @@ import "./mock/Mocked721.sol"; import "forge-std/Test.sol"; contract ApprovalTest is BaseTest { + error InsufficientAllowance(); + function testCannotExecuteTradeIfSenderDidNotApprove() public { address[] memory senderTokenAddresses = new address[](1); senderTokenAddresses[0] = apeAddress; uint256[] memory senderTokenIds = new uint256[](1); senderTokenIds[0] = ape3Id; + uint256[] memory senderTokenAmounts = new uint256[](1); + senderTokenAmounts[0] = 0; address[] memory receiverTokenAddresses = new address[](1); receiverTokenAddresses[0] = birdAddress; uint256[] memory receiverTokenIds = new uint256[](1); receiverTokenIds[0] = bird1Id; + uint256[] memory receiverTokenAmounts = new uint256[](1); + receiverTokenAmounts[0] = 0; + + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); - Offer memory offer = generateOffer( - account3, - senderTokenAddresses, - senderTokenIds, - block.chainid, - account2, - receiverTokenAddresses, - receiverTokenIds, - block.chainid, - in6hours, - OfferState.OPEN - ); + Offer memory offer = generateOffer(account3, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); vm.prank(account3); vm.expectRevert(bytes("NOT_AUTHORIZED")); echo.createOffer(offer); } + function testCannotExecuteTradeIfSenderDidNotApproveERC20() public { + address[] memory senderTokenAddresses = new address[](1); + senderTokenAddresses[0] = wethAddress; + uint256[] memory senderTokenIds = new uint256[](1); + senderTokenIds[0] = 0; + uint256[] memory senderTokenAmounts = new uint256[](1); + senderTokenAmounts[0] = 10 ether; + + address[] memory receiverTokenAddresses = new address[](1); + receiverTokenAddresses[0] = birdAddress; + uint256[] memory receiverTokenIds = new uint256[](1); + receiverTokenIds[0] = bird1Id; + uint256[] memory receiverTokenAmounts = new uint256[](1); + receiverTokenAmounts[0] = 0; + + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); + + Offer memory offer = generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); + + vm.startPrank(account1); + weth.approve(address(echo), 9 ether); + vm.expectRevert(InsufficientAllowance.selector); + echo.createOffer(offer); + } + function testCannotExecuteTradeIfReceiverDidNotApprove() public { address[] memory senderTokenAddresses = new address[](1); senderTokenAddresses[0] = apeAddress; uint256[] memory senderTokenIds = new uint256[](1); senderTokenIds[0] = ape1Id; + uint256[] memory senderTokenAmounts = new uint256[](1); + senderTokenAmounts[0] = 0; address[] memory receiverTokenAddresses = new address[](1); receiverTokenAddresses[0] = birdAddress; uint256[] memory receiverTokenIds = new uint256[](1); receiverTokenIds[0] = bird1Id; + uint256[] memory receiverTokenAmounts = new uint256[](1); + receiverTokenAmounts[0] = 0; - Offer memory offer = generateOffer( - account1, - senderTokenAddresses, - senderTokenIds, - block.chainid, - account2, - receiverTokenAddresses, - receiverTokenIds, - block.chainid, - in6hours, - OfferState.OPEN - ); + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); + + Offer memory offer = generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); vm.prank(account1); echo.createOffer(offer); @@ -72,4 +98,56 @@ contract ApprovalTest is BaseTest { vm.expectRevert(bytes("NOT_AUTHORIZED")); echo.acceptOffer(offerId); } + + function testCannotExecuteTradeIfReceiverDidNotApproveERC20() public { + address[] memory senderTokenAddresses = new address[](3); + senderTokenAddresses[0] = apeAddress; + senderTokenAddresses[1] = apeAddress; + senderTokenAddresses[2] = wethAddress; + uint256[] memory senderTokenIds = new uint256[](3); + senderTokenIds[0] = ape1Id; + senderTokenIds[1] = ape2Id; + senderTokenIds[2] = 0; + uint256[] memory senderTokenAmounts = new uint256[](3); + senderTokenAmounts[0] = 0; + senderTokenAmounts[1] = 0; + senderTokenAmounts[2] = 10 ether; + + address[] memory receiverTokenAddresses = new address[](3); + receiverTokenAddresses[0] = birdAddress; + receiverTokenAddresses[1] = birdAddress; + receiverTokenAddresses[2] = usdtAddress; + uint256[] memory receiverTokenIds = new uint256[](3); + receiverTokenIds[0] = bird1Id; + receiverTokenIds[1] = bird2Id; + receiverTokenIds[2] = 0; + uint256[] memory receiverTokenAmounts = new uint256[](3); + receiverTokenAmounts[0] = 0; + receiverTokenAmounts[1] = 0; + receiverTokenAmounts[2] = 10000; + + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); + + Offer memory offer = generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); + + vm.prank(account1); + echo.createOffer(offer); + vm.stopPrank(); + bytes32 offerId = generateOfferId(offer); + + vm.prank(account2); + usdt.approve(address(echo), 0); + vm.prank(account2); + vm.expectRevert(InsufficientAllowance.selector); + echo.acceptOffer(offerId); + + vm.prank(account2); + usdt.approve(address(echo), 9000); + vm.prank(account2); + vm.expectRevert(InsufficientAllowance.selector); + echo.acceptOffer(offerId); + } } diff --git a/test/BaseTest.t.sol b/test/BaseTest.t.sol index b33e2bc..7e12ad6 100644 --- a/test/BaseTest.t.sol +++ b/test/BaseTest.t.sol @@ -2,8 +2,10 @@ pragma solidity ^0.8.18; import "forge-std/Test.sol"; +import "solady/src/tokens/WETH.sol"; import "./mock/Mocked721.sol"; import "./utils/OfferUtils.sol"; +import "../lib/solady/test/utils/mocks/MockERC20LikeUSDT.sol"; import "../src/Echo.sol"; abstract contract BaseTest is Test, OfferUtils { @@ -19,6 +21,8 @@ abstract contract BaseTest is Test, OfferUtils { Mocked721 public apes; Mocked721 public birds; + MockERC20LikeUSDT public usdt; + WETH public weth; address public constant owner = address(1313); address public constant account1 = address(1337); @@ -29,6 +33,9 @@ abstract contract BaseTest is Test, OfferUtils { address public signer; uint256 public constant signerPrivateKey = 0xB0B; + address public usdtAddress; + address public wethAddress; + address public apeAddress; uint256 public ape1Id; uint256 public ape2Id; @@ -51,6 +58,20 @@ abstract contract BaseTest is Test, OfferUtils { echo = new Echo(address(owner)); + usdt = new MockERC20LikeUSDT(); + weth = new WETH(); + + usdt.mint(account2, 10000); + vm.startPrank(account1); + weth.deposit{value: 10 ether}(); + vm.stopPrank(); + vm.startPrank(account3); + weth.deposit{value: 10 ether}(); + vm.stopPrank(); + + usdtAddress = address(usdt); + wethAddress = address(weth); + apes = new Mocked721("Apes", "APE"); birds = new Mocked721("Birds", "BIRD"); @@ -72,10 +93,12 @@ abstract contract BaseTest is Test, OfferUtils { bird3Id = 3; vm.startPrank(account1); + weth.approve(address(echo), 10 ether); apes.setApprovalForAll(address(echo), true); vm.stopPrank(); vm.startPrank(account2); + usdt.approve(address(echo), 10000); birds.setApprovalForAll(address(echo), true); vm.stopPrank(); @@ -87,24 +110,22 @@ abstract contract BaseTest is Test, OfferUtils { senderTokenAddresses[0] = apeAddress; uint256[] memory senderTokenIds = new uint256[](1); senderTokenIds[0] = ape1Id; + uint256[] memory senderTokenAmounts = new uint256[](1); + senderTokenAmounts[0] = 0; address[] memory receiverTokenAddresses = new address[](1); receiverTokenAddresses[0] = birdAddress; uint256[] memory receiverTokenIds = new uint256[](1); receiverTokenIds[0] = bird1Id; + uint256[] memory receiverTokenAmounts = new uint256[](1); + receiverTokenAmounts[0] = 0; - offer = generateOffer( - account1, - senderTokenAddresses, - senderTokenIds, - block.chainid, - account2, - receiverTokenAddresses, - receiverTokenIds, - block.chainid, - in6hours, - OfferState.OPEN - ); + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); + + offer = generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); bytes32 offerId = generateOfferId(offer); vm.prank(account1); @@ -121,6 +142,9 @@ abstract contract BaseTest is Test, OfferUtils { uint256[] memory senderTokenIds = new uint256[](2); senderTokenIds[0] = ape1Id; senderTokenIds[1] = ape2Id; + uint256[] memory senderTokenAmounts = new uint256[](2); + senderTokenAmounts[0] = 0; + senderTokenAmounts[1] = 0; address[] memory receiverTokenAddresses = new address[](2); receiverTokenAddresses[0] = birdAddress; @@ -128,19 +152,61 @@ abstract contract BaseTest is Test, OfferUtils { uint256[] memory receiverTokenIds = new uint256[](2); receiverTokenIds[0] = bird1Id; receiverTokenIds[1] = bird2Id; + uint256[] memory receiverTokenAmounts = new uint256[](2); + receiverTokenAmounts[0] = 0; + receiverTokenAmounts[1] = 0; + + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); + + offer = generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); + + bytes32 offerId = generateOfferId(offer); + + vm.prank(account1); + vm.expectEmit(true, true, true, true, address(echo)); + emit OfferCreated(offerId); + echo.createOffer(offer); + vm.stopPrank(); + } + + function _createMultiTokensOffer() internal returns (Offer memory offer) { + address[] memory senderTokenAddresses = new address[](3); + senderTokenAddresses[0] = apeAddress; + senderTokenAddresses[1] = apeAddress; + senderTokenAddresses[2] = wethAddress; + uint256[] memory senderTokenIds = new uint256[](3); + senderTokenIds[0] = ape1Id; + senderTokenIds[1] = ape2Id; + senderTokenIds[2] = 0; + uint256[] memory senderTokenAmounts = new uint256[](3); + senderTokenAmounts[0] = 0; + senderTokenAmounts[1] = 0; + senderTokenAmounts[2] = 10 ether; + + address[] memory receiverTokenAddresses = new address[](3); + receiverTokenAddresses[0] = birdAddress; + receiverTokenAddresses[1] = birdAddress; + receiverTokenAddresses[2] = usdtAddress; + + uint256[] memory receiverTokenIds = new uint256[](3); + receiverTokenIds[0] = bird1Id; + receiverTokenIds[1] = bird2Id; + receiverTokenIds[2] = 0; - offer = generateOffer( - account1, - senderTokenAddresses, - senderTokenIds, - block.chainid, - account2, - receiverTokenAddresses, - receiverTokenIds, - block.chainid, - in6hours, - OfferState.OPEN - ); + uint256[] memory receiverTokenAmounts = new uint256[](3); + receiverTokenAmounts[0] = 0; + receiverTokenAmounts[1] = 0; + receiverTokenAmounts[2] = 10000; + + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); + + offer = generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); bytes32 offerId = generateOfferId(offer); @@ -175,6 +241,30 @@ abstract contract BaseTest is Test, OfferUtils { vm.stopPrank(); } + function _createAndAcceptMultiTokensOffer() internal returns (Offer memory offer) { + uint256 tradingFee = echo.tradingFee(); + offer = _createMultiTokensOffer(); + bytes32 offerId = generateOfferId(offer); + + vm.prank(account2); + vm.expectEmit(true, true, true, true, address(echo)); + emit OfferAccepted(offerId); + echo.acceptOffer{value: tradingFee}(offerId); + vm.stopPrank(); + } + + function _executeMultiTokensOffer() internal returns (Offer memory offer) { + uint256 tradingFee = echo.tradingFee(); + offer = _createAndAcceptMultiTokensOffer(); + bytes32 offerId = generateOfferId(offer); + + vm.prank(account1); + vm.expectEmit(true, true, true, true, address(echo)); + emit OfferExecuted(offerId); + echo.executeOffer{value: tradingFee}(offerId); + vm.stopPrank(); + } + function _executeSingleAssetOffer() internal returns (Offer memory offer) { uint256 tradingFee = echo.tradingFee(); offer = _createAndAcceptSingleAssetOffer(); diff --git a/test/CancelOffer.t.sol b/test/CancelOffer.t.sol index 6c3612f..8176b18 100644 --- a/test/CancelOffer.t.sol +++ b/test/CancelOffer.t.sol @@ -11,24 +11,23 @@ contract CancelOfferTest is BaseTest { senderTokenAddresses[0] = apeAddress; uint256[] memory senderTokenIds = new uint256[](1); senderTokenIds[0] = ape2Id; + uint256[] memory senderTokenAmounts = new uint256[](1); + senderTokenAmounts[0] = 0; address[] memory receiverTokenAddresses = new address[](1); receiverTokenAddresses[0] = birdAddress; uint256[] memory receiverTokenIds = new uint256[](1); receiverTokenIds[0] = bird2Id; + uint256[] memory receiverTokenAmounts = new uint256[](1); + receiverTokenAmounts[0] = 0; - Offer memory invalidOffer = generateOffer( - account1, - senderTokenAddresses, - senderTokenIds, - block.chainid, - account2, - receiverTokenAddresses, - receiverTokenIds, - block.chainid, - in6hours, - OfferState.OPEN - ); + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); + + Offer memory invalidOffer = + generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); bytes32 offerId = generateOfferId(invalidOffer); vm.prank(account1); @@ -89,4 +88,25 @@ contract CancelOfferTest is BaseTest { // validate that the receiver items are not in escrow assertOfferItemsOwnership(offer.receiverItems.items, offer.receiver); } + + function testCanCancelOfferMultiTokens() public { + Offer memory offer = _createMultiTokensOffer(); + + bytes32 offerId = generateOfferId(offer); + + vm.prank(account1); + vm.expectEmit(true, true, true, true, address(echo)); + emit OfferCanceled(offerId); + echo.cancelOffer(offerId); + + (address sender,,,,,) = echo.offers(offerId); + + // @dev When the offer is cancelled, it's also deleted + assertEq(sender, address(0)); + + // validate that the sender items are not in escrow + assertOfferItemsOwnership(offer.senderItems.items, offer.sender); + // validate that the receiver items are not in escrow + assertOfferItemsOwnership(offer.receiverItems.items, offer.receiver); + } } diff --git a/test/CreateOffer.t.sol b/test/CreateOffer.t.sol index 8ddf0e0..e5e6dfa 100644 --- a/test/CreateOffer.t.sol +++ b/test/CreateOffer.t.sol @@ -12,24 +12,23 @@ contract CreateOfferTest is BaseTest { senderTokenAddresses[0] = apeAddress; uint256[] memory senderTokenIds = new uint256[](1); senderTokenIds[0] = ape1Id; + uint256[] memory senderTokenAmounts = new uint256[](1); + senderTokenAmounts[0] = 0; address[] memory receiverTokenAddresses = new address[](1); receiverTokenAddresses[0] = birdAddress; uint256[] memory receiverTokenIds = new uint256[](1); receiverTokenIds[0] = bird1Id; + uint256[] memory receiverTokenAmounts = new uint256[](1); + receiverTokenAmounts[0] = 0; - Offer memory offer = generateOffer( - account1, - senderTokenAddresses, - senderTokenIds, - block.chainid, - account2, - receiverTokenAddresses, - receiverTokenIds, - block.chainid, - in6hours, - OfferState.ACCEPTED - ); + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); + + Offer memory offer = + generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.ACCEPTED); vm.prank(account1); vm.expectRevert(InvalidOfferState.selector); @@ -41,24 +40,21 @@ contract CreateOfferTest is BaseTest { senderTokenAddresses[0] = apeAddress; uint256[] memory senderTokenIds = new uint256[](1); senderTokenIds[0] = ape1Id; + uint256[] memory senderTokenAmounts = new uint256[](1); + senderTokenAmounts[0] = 0; address[] memory receiverTokenAddresses = new address[](1); receiverTokenAddresses[0] = birdAddress; uint256[] memory receiverTokenIds = new uint256[](1); receiverTokenIds[0] = bird1Id; + uint256[] memory receiverTokenAmounts = new uint256[](1); + receiverTokenAmounts[0] = 0; - Offer memory offer = generateOffer( - account1, - senderTokenAddresses, - senderTokenIds, - 10, - account2, - receiverTokenAddresses, - receiverTokenIds, - block.chainid, - in6hours, - OfferState.OPEN - ); + OfferItems memory senderItems = generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, 10); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); + + Offer memory offer = generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); vm.prank(account1); vm.expectRevert(InvalidAssets.selector); @@ -70,24 +66,22 @@ contract CreateOfferTest is BaseTest { senderTokenAddresses[0] = apeAddress; uint256[] memory senderTokenIds = new uint256[](1); senderTokenIds[0] = ape1Id; + uint256[] memory senderTokenAmounts = new uint256[](1); + senderTokenAmounts[0] = 0; address[] memory receiverTokenAddresses = new address[](1); receiverTokenAddresses[0] = birdAddress; uint256[] memory receiverTokenIds = new uint256[](1); receiverTokenIds[0] = bird1Id; + uint256[] memory receiverTokenAmounts = new uint256[](1); + receiverTokenAmounts[0] = 0; - Offer memory offer = generateOffer( - account1, - senderTokenAddresses, - senderTokenIds, - block.chainid, - account2, - receiverTokenAddresses, - receiverTokenIds, - 10, - in6hours, - OfferState.OPEN - ); + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, 10); + + Offer memory offer = generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); vm.prank(account1); vm.expectRevert(InvalidAssets.selector); @@ -155,4 +149,35 @@ contract CreateOfferTest is BaseTest { // validate that the receiver items are not in escrow assertOfferItemsOwnership(offer.receiverItems.items, offer.receiver); } + + function testCanCreateOfferMultipleTokens() public { + Offer memory offer = _createMultiTokensOffer(); + + // validate the offer id + bytes32 offerId = generateOfferId(offer); + ( + address sender, + address receiver, + OfferItems memory senderItems, + OfferItems memory receiverItems, + uint256 expiration, + OfferState state + ) = echo.offers(offerId); + assertOfferEq( + offer, + Offer({ + sender: sender, + receiver: receiver, + senderItems: senderItems, + receiverItems: receiverItems, + expiration: expiration, + state: state + }) + ); + + // validate that the sender items are in escrow + assertOfferItemsOwnership(offer.senderItems.items, address(echo)); + // validate that the receiver items are not in escrow + assertOfferItemsOwnership(offer.receiverItems.items, offer.receiver); + } } diff --git a/test/EdgeCasesTest.t.sol b/test/EdgeCasesTest.t.sol new file mode 100644 index 0000000..3cdc8bb --- /dev/null +++ b/test/EdgeCasesTest.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.18; + +import "forge-std/Test.sol"; +import "./BaseTest.t.sol"; +import "../src/types/OfferItems.sol"; +import "../src/types/Offer.sol"; + +contract EdgeCasesTest is BaseTest { + function testCanCreateGhostOffer() public { + // Create an accept an offer + Offer memory initialOffer = _createAndAcceptSingleAssetOffer(); + bytes32 initialOfferId = generateOfferId(initialOffer); + + assertEq(birds.ownerOf(bird1Id), address(echo)); + // Account 2 redeems offer + vm.warp(in6hours); + vm.prank(account2); + echo.redeemOffer(initialOfferId); + vm.stopPrank(); + assertEq(birds.ownerOf(bird1Id), account2); + + // Account 2 creates a new offer with the same NFT + address[] memory senderTokenAddresses = new address[](1); + senderTokenAddresses[0] = birdAddress; + uint256[] memory senderTokenIds = new uint256[](1); + senderTokenIds[0] = bird1Id; + uint256[] memory senderTokenAmounts = new uint256[](1); + senderTokenAmounts[0] = 0; + address[] memory receiverTokenAddresses = new address[](1); + receiverTokenAddresses[0] = apeAddress; + uint256[] memory receiverTokenIds = new uint256[](1); + receiverTokenIds[0] = ape2Id; + uint256[] memory receiverTokenAmounts = new uint256[](1); + receiverTokenAmounts[0] = 0; + + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); + + Offer memory newOffer = generateOffer( + account2, senderItems, account1, receiverItems, block.timestamp + (60 * 60 * 6), OfferState.OPEN + ); + + bytes32 newOfferId = generateOfferId(newOffer); + vm.prank(account2); + echo.createOffer(newOffer); + vm.stopPrank(); + + // Account 1 redeems offer + vm.prank(account1); + echo.redeemOffer(initialOfferId); + vm.stopPrank(); + // Offer is deleted properly + (address senderInitialOffer,,,,,) = echo.offers(initialOfferId); + assertEq(senderInitialOffer, address(0)); + + // Account 1 accept new offer + vm.prank(account1); + echo.acceptOffer(newOfferId); + vm.stopPrank(); + + // Account 2 tries to redeem initial offer again + vm.prank(account2); + echo.redeemOffer(initialOfferId); + vm.stopPrank(); + + // Offer doesn't exist anymore + (,,,, uint256 newExpiration,) = echo.offers(initialOfferId); + assertEq(0, newExpiration); + + // Account 2 now executes new offer + vm.prank(account2); + echo.executeOffer(newOfferId); + + // New offer doesn't exist anymore + (,,,, uint256 latestExpiration,) = echo.offers(newOfferId); + assertEq(0, latestExpiration); + } +} diff --git a/test/ExecuteOffer.t.sol b/test/ExecuteOffer.t.sol index 208f481..ea9dc8d 100644 --- a/test/ExecuteOffer.t.sol +++ b/test/ExecuteOffer.t.sol @@ -11,24 +11,24 @@ contract ExecuteOfferTest is BaseTest { senderTokenAddresses[0] = apeAddress; uint256[] memory senderTokenIds = new uint256[](1); senderTokenIds[0] = ape2Id; + uint256[] memory senderTokenAmounts = new uint256[](1); + senderTokenAmounts[0] = 0; address[] memory receiverTokenAddresses = new address[](1); receiverTokenAddresses[0] = birdAddress; uint256[] memory receiverTokenIds = new uint256[](1); receiverTokenIds[0] = bird2Id; + uint256[] memory receiverTokenAmounts = new uint256[](1); + receiverTokenAmounts[0] = 0; + + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); + + Offer memory invalidOffer = + generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); - Offer memory invalidOffer = generateOffer( - account1, - senderTokenAddresses, - senderTokenIds, - block.chainid, - account2, - receiverTokenAddresses, - receiverTokenIds, - block.chainid, - in6hours, - OfferState.OPEN - ); bytes32 offerId = generateOfferId(invalidOffer); vm.prank(account1); @@ -81,4 +81,19 @@ contract ExecuteOfferTest is BaseTest { // validate that the receiver items are swapped assertOfferItemsOwnership(offer.receiverItems.items, offer.sender); } + + function testCanExecuteOfferMultiTokens() public { + Offer memory offer = _executeMultiTokensOffer(); + bytes32 offerId = generateOfferId(offer); + + (address sender,,,,,) = echo.offers(offerId); + + // @dev When the offer is executed, it's also deleted + assertEq(sender, address(0)); + + // validate that the sender items are swapped + assertOfferItemsOwnership(offer.senderItems.items, offer.receiver); + // validate that the receiver items are swapped + assertOfferItemsOwnership(offer.receiverItems.items, offer.sender); + } } diff --git a/test/Expiration.t.sol b/test/Expiration.t.sol index 7b9d47c..f9f5ce3 100644 --- a/test/Expiration.t.sol +++ b/test/Expiration.t.sol @@ -10,24 +10,23 @@ contract ExpirationTest is BaseTest { senderTokenAddresses[0] = apeAddress; uint256[] memory senderTokenIds = new uint256[](1); senderTokenIds[0] = ape1Id; + uint256[] memory senderTokenAmounts = new uint256[](1); + senderTokenAmounts[0] = 0; address[] memory receiverTokenAddresses = new address[](1); receiverTokenAddresses[0] = birdAddress; uint256[] memory receiverTokenIds = new uint256[](1); receiverTokenIds[0] = bird1Id; + uint256[] memory receiverTokenAmounts = new uint256[](1); + receiverTokenAmounts[0] = 0; - Offer memory offer = generateOffer( - account1, - senderTokenAddresses, - senderTokenIds, - block.chainid, - account2, - receiverTokenAddresses, - receiverTokenIds, - block.chainid, - block.timestamp, - OfferState.OPEN - ); + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); + + Offer memory offer = + generateOffer(account1, senderItems, account2, receiverItems, block.timestamp, OfferState.OPEN); vm.prank(account1); vm.expectRevert(OfferHasExpired.selector); diff --git a/test/Ownership.t.sol b/test/Ownership.t.sol index 6556b02..aa7ac84 100644 --- a/test/Ownership.t.sol +++ b/test/Ownership.t.sol @@ -10,24 +10,22 @@ contract OwnershipTest is BaseTest { senderTokenAddresses[0] = apeAddress; uint256[] memory senderTokenIds = new uint256[](1); senderTokenIds[0] = ape1Id; + uint256[] memory senderTokenAmounts = new uint256[](1); + senderTokenAmounts[0] = 0; address[] memory receiverTokenAddresses = new address[](1); receiverTokenAddresses[0] = birdAddress; uint256[] memory receiverTokenIds = new uint256[](1); receiverTokenIds[0] = bird1Id; + uint256[] memory receiverTokenAmounts = new uint256[](1); + receiverTokenAmounts[0] = 0; - Offer memory offer = generateOffer( - account1, - senderTokenAddresses, - senderTokenIds, - block.chainid, - account2, - receiverTokenAddresses, - receiverTokenIds, - block.chainid, - in6hours, - OfferState.OPEN - ); + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); + + Offer memory offer = generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); vm.prank(account2); vm.expectRevert(InvalidSender.selector); diff --git a/test/Paused.t.sol b/test/Paused.t.sol index 39dab47..09f6854 100644 --- a/test/Paused.t.sol +++ b/test/Paused.t.sol @@ -12,24 +12,22 @@ contract PausedTest is BaseTest { senderTokenAddresses[0] = apeAddress; uint256[] memory senderTokenIds = new uint256[](1); senderTokenIds[0] = ape1Id; + uint256[] memory senderTokenAmounts = new uint256[](1); + senderTokenAmounts[0] = 0; address[] memory receiverTokenAddresses = new address[](1); receiverTokenAddresses[0] = birdAddress; uint256[] memory receiverTokenIds = new uint256[](1); receiverTokenIds[0] = bird1Id; + uint256[] memory receiverTokenAmounts = new uint256[](1); + receiverTokenAmounts[0] = 0; - Offer memory offer = generateOffer( - account1, - senderTokenAddresses, - senderTokenIds, - block.chainid, - account2, - receiverTokenAddresses, - receiverTokenIds, - block.chainid, - in6hours, - OfferState.OPEN - ); + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); + + Offer memory offer = generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); vm.prank(account1); vm.expectRevert(Paused.selector); @@ -43,24 +41,22 @@ contract PausedTest is BaseTest { senderTokenAddresses[0] = apeAddress; uint256[] memory senderTokenIds = new uint256[](1); senderTokenIds[0] = ape1Id; + uint256[] memory senderTokenAmounts = new uint256[](1); + senderTokenAmounts[0] = 0; address[] memory receiverTokenAddresses = new address[](1); receiverTokenAddresses[0] = birdAddress; uint256[] memory receiverTokenIds = new uint256[](1); receiverTokenIds[0] = bird1Id; + uint256[] memory receiverTokenAmounts = new uint256[](1); + receiverTokenAmounts[0] = 0; + + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); - Offer memory offer = generateOffer( - account1, - senderTokenAddresses, - senderTokenIds, - block.chainid, - account2, - receiverTokenAddresses, - receiverTokenIds, - block.chainid, - in6hours, - OfferState.OPEN - ); + Offer memory offer = generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); vm.prank(account1); vm.expectRevert(CreationPaused.selector); diff --git a/test/RedeemOffer.t.sol b/test/RedeemOffer.t.sol index 96785c1..22329d5 100644 --- a/test/RedeemOffer.t.sol +++ b/test/RedeemOffer.t.sol @@ -200,4 +200,34 @@ contract RedeemOfferTest is BaseTest { // @dev When the offer is redeemed, it's also deleted assertEq(sender, address(0)); } + + function testCanFullyRedeemExpiredAcceptedOfferMultiToken() public { + Offer memory offer = _createAndAcceptMultiTokensOffer(); + bytes32 offerId = generateOfferId(offer); + + // validate that the receiver items are escrowed + assertOfferItemsOwnership(offer.receiverItems.items, address(echo)); + // validate that the sender items are escrowed + assertOfferItemsOwnership(offer.senderItems.items, address(echo)); + + vm.warp(in6hours); + + vm.prank(account2); + echo.redeemOffer(offerId); + // validate that the receiver items are redemeed + assertOfferItemsOwnership(offer.receiverItems.items, offer.receiver); + (, address receiver,,,,) = echo.offers(offerId); + + // @dev Offer is still existing because sender has not redeemed + assertEq(receiver, offer.receiver); + + vm.prank(account1); + echo.redeemOffer(offerId); + // validate that the receiver items are redemeed + assertOfferItemsOwnership(offer.senderItems.items, offer.sender); + + (address sender,,,,,) = echo.offers(offerId); + // @dev Offer does not exist + assertEq(sender, address(0)); + } } diff --git a/test/WrongAssets.t.sol b/test/WrongAssets.t.sol index 7af57cb..ccd1564 100644 --- a/test/WrongAssets.t.sol +++ b/test/WrongAssets.t.sol @@ -10,22 +10,19 @@ contract WrongAssetsTest is BaseTest { senderTokenAddresses[0] = apeAddress; uint256[] memory senderTokenIds = new uint256[](1); senderTokenIds[0] = ape1Id; + uint256[] memory senderTokenAmounts = new uint256[](1); + senderTokenAmounts[0] = 0; address[] memory receiverTokenAddresses = new address[](0); uint256[] memory receiverTokenIds = new uint256[](0); + uint256[] memory receiverTokenAmounts = new uint256[](0); - Offer memory offer = generateOffer( - account1, - senderTokenAddresses, - senderTokenIds, - block.chainid, - account2, - receiverTokenAddresses, - receiverTokenIds, - block.chainid, - in6hours, - OfferState.OPEN - ); + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); + + Offer memory offer = generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); vm.prank(account1); vm.expectRevert(InvalidAssets.selector); @@ -35,24 +32,21 @@ contract WrongAssetsTest is BaseTest { function testCannotCreateOfferIfSenderItemsIsEmpty() public { address[] memory senderTokenAddresses = new address[](0); uint256[] memory senderTokenIds = new uint256[](0); + uint256[] memory senderTokenAmounts = new uint256[](0); address[] memory receiverTokenAddresses = new address[](1); receiverTokenAddresses[0] = birdAddress; uint256[] memory receiverTokenIds = new uint256[](1); receiverTokenIds[0] = bird1Id; + uint256[] memory receiverTokenAmounts = new uint256[](1); + receiverTokenAmounts[0] = 0; + + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); - Offer memory offer = generateOffer( - account1, - senderTokenAddresses, - senderTokenIds, - block.chainid, - account2, - receiverTokenAddresses, - receiverTokenIds, - block.chainid, - in6hours, - OfferState.OPEN - ); + Offer memory offer = generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); vm.prank(account1); vm.expectRevert(InvalidAssets.selector); @@ -66,24 +60,23 @@ contract WrongAssetsTest is BaseTest { uint256[] memory senderTokenIds = new uint256[](2); senderTokenIds[0] = ape1Id; senderTokenIds[1] = ape1Id; + uint256[] memory senderTokenAmounts = new uint256[](2); + senderTokenAmounts[0] = 0; + senderTokenAmounts[1] = 0; address[] memory receiverTokenAddresses = new address[](1); receiverTokenAddresses[0] = birdAddress; uint256[] memory receiverTokenIds = new uint256[](1); receiverTokenIds[0] = bird1Id; + uint256[] memory receiverTokenAmounts = new uint256[](1); + receiverTokenAmounts[0] = 0; - Offer memory offer = generateOffer( - account1, - senderTokenAddresses, - senderTokenIds, - block.chainid, - account2, - receiverTokenAddresses, - receiverTokenIds, - block.chainid, - in6hours, - OfferState.OPEN - ); + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); + + Offer memory offer = generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); vm.prank(account1); vm.expectRevert("WRONG_FROM"); @@ -95,6 +88,8 @@ contract WrongAssetsTest is BaseTest { senderTokenAddresses[0] = apeAddress; uint256[] memory senderTokenIds = new uint256[](1); senderTokenIds[0] = ape1Id; + uint256[] memory senderTokenAmounts = new uint256[](1); + senderTokenAmounts[0] = 0; address[] memory receiverTokenAddresses = new address[](2); receiverTokenAddresses[0] = birdAddress; @@ -102,19 +97,16 @@ contract WrongAssetsTest is BaseTest { uint256[] memory receiverTokenIds = new uint256[](2); receiverTokenIds[0] = bird1Id; receiverTokenIds[1] = bird1Id; + uint256[] memory receiverTokenAmounts = new uint256[](2); + receiverTokenAmounts[0] = 0; + receiverTokenAmounts[1] = 0; + + OfferItems memory senderItems = + generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); + OfferItems memory receiverItems = + generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); - Offer memory offer = generateOffer( - account1, - senderTokenAddresses, - senderTokenIds, - block.chainid, - account2, - receiverTokenAddresses, - receiverTokenIds, - block.chainid, - in6hours, - OfferState.OPEN - ); + Offer memory offer = generateOffer(account1, senderItems, account2, receiverItems, in6hours, OfferState.OPEN); vm.prank(account1); echo.createOffer(offer); diff --git a/test/utils/OfferUtils.sol b/test/utils/OfferUtils.sol index 2b22558..22d86fe 100644 --- a/test/utils/OfferUtils.sol +++ b/test/utils/OfferUtils.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.18; import "forge-std/Test.sol"; +import "solady/src/tokens/ERC20.sol"; import "../mock/Mocked721.sol"; import "../../src/types/Offer.sol"; import "../../src/types/OfferItem.sol"; @@ -11,9 +12,7 @@ abstract contract OfferUtils is Test { // Exclude from coverage report function test() public virtual {} - OfferItem[] public senderItems; - OfferItem[] public receiverItems; - + OfferItem[] private items; uint256 public in6hours; function generateOfferId(Offer memory offer) public pure returns (bytes32 offerId) { @@ -30,44 +29,49 @@ abstract contract OfferUtils is Test { ); } + function generateOfferItems( + address[] memory tokenAddresses, + uint256[] memory tokenIds, + uint256[] memory tokenAmounts, + uint256 chainId + ) public returns (OfferItems memory offerItems) { + require(tokenAddresses.length == tokenIds.length, "Items arrays do not match"); + require(tokenAddresses.length == tokenAmounts.length, "Items arrays do not match"); + delete items; + for (uint256 i = 0; i < tokenAddresses.length; i++) { + items.push(OfferItem({tokenAddress: tokenAddresses[i], tokenId: tokenIds[i], amount: tokenAmounts[i]})); + } + offerItems = OfferItems({chainId: chainId, items: items}); + } + function generateOffer( address sender, - address[] memory senderTokenAddresses, - uint256[] memory senderTokenIds, - uint256 senderChainId, + OfferItems memory senderItems, address receiver, - address[] memory receiverTokenAddresses, - uint256[] memory receiverTokenIds, - uint256 receiverChainId, + OfferItems memory receiverItems, uint256 expiration, OfferState state - ) public returns (Offer memory offer) { - require(senderTokenAddresses.length == senderTokenIds.length, "Sender items arrays do not match"); - - require(receiverTokenAddresses.length == receiverTokenIds.length, "Receiver items arrays do not match"); - - for (uint256 i = 0; i < senderTokenAddresses.length; i++) { - senderItems.push(OfferItem({tokenAddress: senderTokenAddresses[i], tokenId: senderTokenIds[i]})); - } - - for (uint256 i = 0; i < receiverTokenAddresses.length; i++) { - receiverItems.push(OfferItem({tokenAddress: receiverTokenAddresses[i], tokenId: receiverTokenIds[i]})); - } - + ) public pure returns (Offer memory offer) { offer = Offer({ sender: sender, receiver: receiver, - senderItems: OfferItems({chainId: senderChainId, items: senderItems}), - receiverItems: OfferItems({chainId: receiverChainId, items: receiverItems}), + senderItems: senderItems, + receiverItems: receiverItems, expiration: expiration, state: state }); } - function assertOfferItemsOwnership(OfferItem[] memory items, address owner) public { - for (uint256 i = 0; i < items.length; i++) { - Mocked721 tokenContract = Mocked721(items[i].tokenAddress); - assertEq(tokenContract.ownerOf(items[i].tokenId), owner); + function assertOfferItemsOwnership(OfferItem[] memory _items, address owner) public { + for (uint256 i = 0; i < _items.length; i++) { + OfferItem memory item = _items[i]; + if (item.amount >= 1) { + ERC20 tokenContract = ERC20(item.tokenAddress); + assertEq(tokenContract.balanceOf(owner), item.amount); + } else { + Mocked721 tokenContract = Mocked721(item.tokenAddress); + assertEq(tokenContract.ownerOf(item.tokenId), owner); + } } } @@ -77,6 +81,7 @@ abstract contract OfferUtils is Test { for (uint256 i = 0; i < items1.items.length; i++) { assertEq(items1.items[i].tokenAddress, items2.items[i].tokenAddress); assertEq(items1.items[i].tokenId, items2.items[i].tokenId); + assertEq(items1.items[i].amount, items2.items[i].amount); } } From d7d6579adb56995842ec49383b0769e7ba986034 Mon Sep 17 00:00:00 2001 From: Gabriel Cartier <4060669+GabrielCartier@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:38:08 -0400 Subject: [PATCH 3/6] removed edge case testing for now, not really useful --- test/EdgeCasesTest.t.sol | 81 ---------------------------------------- 1 file changed, 81 deletions(-) delete mode 100644 test/EdgeCasesTest.t.sol diff --git a/test/EdgeCasesTest.t.sol b/test/EdgeCasesTest.t.sol deleted file mode 100644 index 3cdc8bb..0000000 --- a/test/EdgeCasesTest.t.sol +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.18; - -import "forge-std/Test.sol"; -import "./BaseTest.t.sol"; -import "../src/types/OfferItems.sol"; -import "../src/types/Offer.sol"; - -contract EdgeCasesTest is BaseTest { - function testCanCreateGhostOffer() public { - // Create an accept an offer - Offer memory initialOffer = _createAndAcceptSingleAssetOffer(); - bytes32 initialOfferId = generateOfferId(initialOffer); - - assertEq(birds.ownerOf(bird1Id), address(echo)); - // Account 2 redeems offer - vm.warp(in6hours); - vm.prank(account2); - echo.redeemOffer(initialOfferId); - vm.stopPrank(); - assertEq(birds.ownerOf(bird1Id), account2); - - // Account 2 creates a new offer with the same NFT - address[] memory senderTokenAddresses = new address[](1); - senderTokenAddresses[0] = birdAddress; - uint256[] memory senderTokenIds = new uint256[](1); - senderTokenIds[0] = bird1Id; - uint256[] memory senderTokenAmounts = new uint256[](1); - senderTokenAmounts[0] = 0; - address[] memory receiverTokenAddresses = new address[](1); - receiverTokenAddresses[0] = apeAddress; - uint256[] memory receiverTokenIds = new uint256[](1); - receiverTokenIds[0] = ape2Id; - uint256[] memory receiverTokenAmounts = new uint256[](1); - receiverTokenAmounts[0] = 0; - - OfferItems memory senderItems = - generateOfferItems(senderTokenAddresses, senderTokenIds, senderTokenAmounts, block.chainid); - OfferItems memory receiverItems = - generateOfferItems(receiverTokenAddresses, receiverTokenIds, receiverTokenAmounts, block.chainid); - - Offer memory newOffer = generateOffer( - account2, senderItems, account1, receiverItems, block.timestamp + (60 * 60 * 6), OfferState.OPEN - ); - - bytes32 newOfferId = generateOfferId(newOffer); - vm.prank(account2); - echo.createOffer(newOffer); - vm.stopPrank(); - - // Account 1 redeems offer - vm.prank(account1); - echo.redeemOffer(initialOfferId); - vm.stopPrank(); - // Offer is deleted properly - (address senderInitialOffer,,,,,) = echo.offers(initialOfferId); - assertEq(senderInitialOffer, address(0)); - - // Account 1 accept new offer - vm.prank(account1); - echo.acceptOffer(newOfferId); - vm.stopPrank(); - - // Account 2 tries to redeem initial offer again - vm.prank(account2); - echo.redeemOffer(initialOfferId); - vm.stopPrank(); - - // Offer doesn't exist anymore - (,,,, uint256 newExpiration,) = echo.offers(initialOfferId); - assertEq(0, newExpiration); - - // Account 2 now executes new offer - vm.prank(account2); - echo.executeOffer(newOfferId); - - // New offer doesn't exist anymore - (,,,, uint256 latestExpiration,) = echo.offers(newOfferId); - assertEq(0, latestExpiration); - } -} From 244d4a13daf384ab6e14c7a119620e735fb42bcf Mon Sep 17 00:00:00 2001 From: Gabriel Cartier <4060669+GabrielCartier@users.noreply.github.com> Date: Wed, 10 Jul 2024 14:47:29 -0400 Subject: [PATCH 4/6] added safe transfer for erc20 --- src/escrow/EscrowHandler.sol | 7 +++---- test/Approval.t.sol | 8 ++++---- test/mock/YieldMock.sol | 3 +++ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/escrow/EscrowHandler.sol b/src/escrow/EscrowHandler.sol index 565ef70..ea14ffc 100644 --- a/src/escrow/EscrowHandler.sol +++ b/src/escrow/EscrowHandler.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.18; import "solmate/tokens/ERC721.sol"; -import "solady/src/tokens/ERC20.sol"; import "../types/OfferItems.sol"; +import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; abstract contract EscrowHandler is ERC721TokenReceiver { function _transferERC721(address collectionAddress, uint256 id, address from, address to) internal { @@ -12,11 +12,10 @@ abstract contract EscrowHandler is ERC721TokenReceiver { } function _transferERC20(address tokenAddress, uint256 amount, address from, address to) internal { - ERC20 token = ERC20(tokenAddress); if (address(this) == from) { - token.transfer(to, amount); + SafeTransferLib.safeTransfer(tokenAddress, to, amount); } else { - token.transferFrom(from, to, amount); + SafeTransferLib.safeTransferFrom(tokenAddress, from, to, amount); } } diff --git a/test/Approval.t.sol b/test/Approval.t.sol index 27fdf38..68a5953 100644 --- a/test/Approval.t.sol +++ b/test/Approval.t.sol @@ -6,7 +6,7 @@ import "./mock/Mocked721.sol"; import "forge-std/Test.sol"; contract ApprovalTest is BaseTest { - error InsufficientAllowance(); + error TransferFromFailed(); function testCannotExecuteTradeIfSenderDidNotApprove() public { address[] memory senderTokenAddresses = new address[](1); @@ -59,7 +59,7 @@ contract ApprovalTest is BaseTest { vm.startPrank(account1); weth.approve(address(echo), 9 ether); - vm.expectRevert(InsufficientAllowance.selector); + vm.expectRevert(TransferFromFailed.selector); echo.createOffer(offer); } @@ -141,13 +141,13 @@ contract ApprovalTest is BaseTest { vm.prank(account2); usdt.approve(address(echo), 0); vm.prank(account2); - vm.expectRevert(InsufficientAllowance.selector); + vm.expectRevert(TransferFromFailed.selector); echo.acceptOffer(offerId); vm.prank(account2); usdt.approve(address(echo), 9000); vm.prank(account2); - vm.expectRevert(InsufficientAllowance.selector); + vm.expectRevert(TransferFromFailed.selector); echo.acceptOffer(offerId); } } diff --git a/test/mock/YieldMock.sol b/test/mock/YieldMock.sol index d63a17d..db66076 100644 --- a/test/mock/YieldMock.sol +++ b/test/mock/YieldMock.sol @@ -2,6 +2,9 @@ pragma solidity ^0.8.18; contract YieldMock { + // Exclude from coverage report + function test() public virtual {} + address private constant blastContract = 0x4300000000000000000000000000000000000002; mapping(address => uint8) public getConfiguration; From 55150879aa150f136c14478ea54e3ece31c29dae Mon Sep 17 00:00:00 2001 From: Gabriel Cartier <4060669+GabrielCartier@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:53:23 -0400 Subject: [PATCH 5/6] added blast testing, not complete --- src/EchoBlast.sol | 2 +- test/AdminBlast.t.sol | 20 ++++++++++++++++++++ test/BaseTest.t.sol | 2 +- test/BaseTestBlast.t.sol | 25 +++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 test/AdminBlast.t.sol create mode 100644 test/BaseTestBlast.t.sol diff --git a/src/EchoBlast.sol b/src/EchoBlast.sol index 1664e8f..9c95272 100644 --- a/src/EchoBlast.sol +++ b/src/EchoBlast.sol @@ -6,7 +6,7 @@ import "../lib/blast/IBlast.sol"; import "../lib/blast/IBlastPoints.sol"; contract EchoBlast is Echo { - IBlast public constant BLAST = IBlast(0x4300000000000000000000000000000000000002); + IBlast private constant BLAST = IBlast(0x4300000000000000000000000000000000000002); constructor(address owner, address blastPointsAddress) Echo(owner) { IBlastPoints(blastPointsAddress).configurePointsOperator(owner); diff --git a/test/AdminBlast.t.sol b/test/AdminBlast.t.sol new file mode 100644 index 0000000..ea48760 --- /dev/null +++ b/test/AdminBlast.t.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.18; + +import "forge-std/Test.sol"; +import "./BaseTestBlast.t.sol"; + +contract AdminBlastTest is BaseTestBlast { + function testCannotClaimGasIfNotOwner() public { + vm.prank(account1); + vm.expectRevert("UNAUTHORIZED"); + echoBlast.claimGas(); + } + + // TODO Should be able to test that properly + function testCanClaimGasIfNotOwner() public { + vm.prank(owner); + vm.expectRevert(bytes("must withdraw non-zero amount")); + echoBlast.claimGas(); + } +} diff --git a/test/BaseTest.t.sol b/test/BaseTest.t.sol index 7e12ad6..37bcaf2 100644 --- a/test/BaseTest.t.sol +++ b/test/BaseTest.t.sol @@ -45,7 +45,7 @@ abstract contract BaseTest is Test, OfferUtils { uint256 public bird2Id; uint256 public bird3Id; - function setUp() public { + function setUp() public virtual { // Generate account2 and signer from private key. account2 = vm.addr(account2PrivateKey); signer = vm.addr(signerPrivateKey); diff --git a/test/BaseTestBlast.t.sol b/test/BaseTestBlast.t.sol new file mode 100644 index 0000000..4091ef5 --- /dev/null +++ b/test/BaseTestBlast.t.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.18; + +import "forge-std/Test.sol"; +import "../src/EchoBlast.sol"; +import "./mock/YieldMock.sol"; + +abstract contract BaseTestBlast is Test { + address public constant owner = address(1313); + address public constant account1 = address(1337); + EchoBlast public echoBlast; + + function setUp() public { + vm.createSelectFork("https://sepolia.blast.io"); + // Deploy mock of the precompile + YieldMock yieldMock = new YieldMock(); + // Set mock bytecode to the expected precompile address + vm.etch(0x0000000000000000000000000000000000000100, address(yieldMock).code); + + // Fund accounts + vm.deal(account1, 100 ether); + echoBlast = + new EchoBlast({owner: owner, blastPointsAddress: address(0x2fc95838c71e76ec69ff817983BFf17c710F34E0)}); + } +} From 04e8936890c6d926f09f80bfc3601c098ae338b2 Mon Sep 17 00:00:00 2001 From: Gabriel Cartier <4060669+GabrielCartier@users.noreply.github.com> Date: Mon, 15 Jul 2024 18:54:21 -0400 Subject: [PATCH 6/6] added token type --- script/TestPacking.s.sol | 6 +++++- src/Echo.sol | 26 +++++++------------------- src/EchoBlast.sol | 4 ++-- src/escrow/EscrowHandler.sol | 9 +++++---- src/types/Offer.sol | 3 ++- src/types/OfferItem.sol | 10 +++++++--- test/utils/OfferUtils.sol | 31 +++++++++++++++++++++++++------ 7 files changed, 53 insertions(+), 36 deletions(-) diff --git a/script/TestPacking.s.sol b/script/TestPacking.s.sol index 18487bf..4ed59ed 100644 --- a/script/TestPacking.s.sol +++ b/script/TestPacking.s.sol @@ -10,7 +10,11 @@ contract TestPacking is Script { function run() external view { OfferItem[] memory offerItems = new OfferItem[](1); - offerItems[0] = OfferItem({tokenAddress: 0x7DA16cd402106Adaf39092215DbB54092b80B6E6, tokenId: 2, amount: 0}); + offerItems[0] = OfferItem({ + tokenAddress: 0x7DA16cd402106Adaf39092215DbB54092b80B6E6, + tokenIdOrAmount: 2, + tokenType: TokenType.ERC721 + }); Offer memory offer = Offer({ sender: 0x7DA16cd402106Adaf39092215DbB54092b80B6E6, diff --git a/src/Echo.sol b/src/Echo.sol index 83afb08..cce3402 100644 --- a/src/Echo.sol +++ b/src/Echo.sol @@ -103,29 +103,17 @@ contract Echo is ReentrancyGuard, Admin, Banker, Escrow, EchoState { revert OfferHasNotExpired(); } + // @dev Receiver has escrowed only if offer was PARTLY_REDEEMED + if (offer.state == OfferState.ACCEPTED) { + offers[offerId].state = OfferState.PARTLY_REDEEMED; + } else { + delete offers[offerId]; + } + // @dev If sender, we need extra checks to make sure receiver also redeemed if offer was accepted if (msg.sender == offer.sender) { - // @dev Receiver has escrowed only if offer was accepted - if (offer.state == OfferState.ACCEPTED) { - OfferItem memory receiverFirstOfferItem = offer.receiverItems.items[0]; - ERC721 receiverFirstNft = ERC721(receiverFirstOfferItem.tokenAddress); - // @dev if Echo is not the owner, it means receiver has redeemed - if (receiverFirstNft.ownerOf(receiverFirstOfferItem.tokenId) != address(this)) { - delete offers[offerId]; - } - // @dev If offer was OPEN, receiver has not escrowed, we can safely delete - } else { - delete offers[offerId]; - } _withdraw(offer.senderItems, offer.sender); } else { - // @dev We need to check if sender has redeemed too - OfferItem memory senderFirstOfferItem = offer.senderItems.items[0]; - ERC721 senderFirstNft = ERC721(senderFirstOfferItem.tokenAddress); - // @dev if Echo is not the owner, it means sender has redeemed - if (senderFirstNft.ownerOf(senderFirstOfferItem.tokenId) != address(this)) { - delete offers[offerId]; - } _withdraw(offer.receiverItems, offer.receiver); } emit OfferRedeeemed(offerId, msg.sender); diff --git a/src/EchoBlast.sol b/src/EchoBlast.sol index 9c95272..0b96175 100644 --- a/src/EchoBlast.sol +++ b/src/EchoBlast.sol @@ -14,7 +14,7 @@ contract EchoBlast is Echo { BLAST.configureClaimableGas(); } - function claimGas() external onlyOwner { - BLAST.claimMaxGas(address(this), msg.sender); + function claimGas() external onlyOwner returns (uint256) { + return BLAST.claimMaxGas(address(this), msg.sender); } } diff --git a/src/escrow/EscrowHandler.sol b/src/escrow/EscrowHandler.sol index ea14ffc..10b7871 100644 --- a/src/escrow/EscrowHandler.sol +++ b/src/escrow/EscrowHandler.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.18; +import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; import "solmate/tokens/ERC721.sol"; +import "../types/OfferItem.sol"; import "../types/OfferItems.sol"; -import {SafeTransferLib} from "solady/src/utils/SafeTransferLib.sol"; abstract contract EscrowHandler is ERC721TokenReceiver { function _transferERC721(address collectionAddress, uint256 id, address from, address to) internal { @@ -24,10 +25,10 @@ abstract contract EscrowHandler is ERC721TokenReceiver { uint256 length = offerItems.items.length; for (uint256 i = 0; i < length;) { OfferItem memory item = offerItems.items[i]; - if (item.amount >= 1) { - _transferERC20(item.tokenAddress, item.amount, from, to); + if (item.tokenType == TokenType.ERC20) { + _transferERC20(item.tokenAddress, item.tokenIdOrAmount, from, to); } else { - _transferERC721(item.tokenAddress, item.tokenId, from, to); + _transferERC721(item.tokenAddress, item.tokenIdOrAmount, from, to); } unchecked { i++; diff --git a/src/types/Offer.sol b/src/types/Offer.sol index de32f43..b1eded2 100644 --- a/src/types/Offer.sol +++ b/src/types/Offer.sol @@ -5,7 +5,8 @@ import "./OfferItems.sol"; enum OfferState { OPEN, - ACCEPTED + ACCEPTED, + PARTLY_REDEEMED } // @dev Struct representing an on-chain offer diff --git a/src/types/OfferItem.sol b/src/types/OfferItem.sol index 70e5feb..2730b9b 100644 --- a/src/types/OfferItem.sol +++ b/src/types/OfferItem.sol @@ -1,11 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.18; +enum TokenType { + ERC20, + ERC721 +} + // @dev Struct representing an offer item // @dev We support ERC721 and ERC20 -// @dev If amount is 0, then we use tokenId. struct OfferItem { address tokenAddress; - uint256 tokenId; - uint256 amount; + TokenType tokenType; + uint256 tokenIdOrAmount; } diff --git a/test/utils/OfferUtils.sol b/test/utils/OfferUtils.sol index 22d86fe..aa61a21 100644 --- a/test/utils/OfferUtils.sol +++ b/test/utils/OfferUtils.sol @@ -29,6 +29,7 @@ abstract contract OfferUtils is Test { ); } + // TODO Should receive an array of types function generateOfferItems( address[] memory tokenAddresses, uint256[] memory tokenIds, @@ -39,7 +40,23 @@ abstract contract OfferUtils is Test { require(tokenAddresses.length == tokenAmounts.length, "Items arrays do not match"); delete items; for (uint256 i = 0; i < tokenAddresses.length; i++) { - items.push(OfferItem({tokenAddress: tokenAddresses[i], tokenId: tokenIds[i], amount: tokenAmounts[i]})); + if (tokenAmounts[i] > 0) { + items.push( + OfferItem({ + tokenAddress: tokenAddresses[i], + tokenIdOrAmount: tokenAmounts[i], + tokenType: TokenType.ERC20 + }) + ); + } else { + items.push( + OfferItem({ + tokenAddress: tokenAddresses[i], + tokenIdOrAmount: tokenIds[i], + tokenType: TokenType.ERC721 + }) + ); + } } offerItems = OfferItems({chainId: chainId, items: items}); } @@ -65,12 +82,12 @@ abstract contract OfferUtils is Test { function assertOfferItemsOwnership(OfferItem[] memory _items, address owner) public { for (uint256 i = 0; i < _items.length; i++) { OfferItem memory item = _items[i]; - if (item.amount >= 1) { + if (item.tokenType == TokenType.ERC20) { ERC20 tokenContract = ERC20(item.tokenAddress); - assertEq(tokenContract.balanceOf(owner), item.amount); + assertEq(tokenContract.balanceOf(owner), item.tokenIdOrAmount); } else { Mocked721 tokenContract = Mocked721(item.tokenAddress); - assertEq(tokenContract.ownerOf(item.tokenId), owner); + assertEq(tokenContract.ownerOf(item.tokenIdOrAmount), owner); } } } @@ -80,8 +97,10 @@ abstract contract OfferUtils is Test { assertEq(items1.chainId, items2.chainId); for (uint256 i = 0; i < items1.items.length; i++) { assertEq(items1.items[i].tokenAddress, items2.items[i].tokenAddress); - assertEq(items1.items[i].tokenId, items2.items[i].tokenId); - assertEq(items1.items[i].amount, items2.items[i].amount); + assertEq(items1.items[i].tokenIdOrAmount, items2.items[i].tokenIdOrAmount); + if (items1.items[i].tokenType != items2.items[i].tokenType) { + assertTrue(false); + } } }