diff --git a/script/TestPacking.s.sol b/script/TestPacking.s.sol index 6d75667..4ed59ed 100644 --- a/script/TestPacking.s.sol +++ b/script/TestPacking.s.sol @@ -8,9 +8,13 @@ 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, + tokenIdOrAmount: 2, + tokenType: TokenType.ERC721 + }); 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..8a5db29 --- /dev/null +++ b/script/offer/CreateTestOffer.s.sol @@ -0,0 +1,44 @@ +// 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; + + 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..cce3402 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(); @@ -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 1664e8f..0b96175 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); @@ -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 5fbe750..10b7871 100644 --- a/src/escrow/EscrowHandler.sol +++ b/src/escrow/EscrowHandler.sol @@ -1,7 +1,9 @@ // 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"; abstract contract EscrowHandler is ERC721TokenReceiver { @@ -10,11 +12,24 @@ 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 { + if (address(this) == from) { + SafeTransferLib.safeTransfer(tokenAddress, to, amount); + } else { + SafeTransferLib.safeTransferFrom(tokenAddress, 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.tokenType == TokenType.ERC20) { + _transferERC20(item.tokenAddress, item.tokenIdOrAmount, from, to); + } else { + _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 f90ad9b..2730b9b 100644 --- a/src/types/OfferItem.sol +++ b/src/types/OfferItem.sol @@ -1,9 +1,15 @@ // 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 +enum TokenType { + ERC20, + ERC721 +} + +// @dev Struct representing an offer item +// @dev We support ERC721 and ERC20 struct OfferItem { address tokenAddress; - uint256 tokenId; + TokenType tokenType; + uint256 tokenIdOrAmount; } 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/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/Approval.t.sol b/test/Approval.t.sol index 88ac270..68a5953 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 TransferFromFailed(); + 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(TransferFromFailed.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(TransferFromFailed.selector); + echo.acceptOffer(offerId); + + vm.prank(account2); + usdt.approve(address(echo), 9000); + vm.prank(account2); + vm.expectRevert(TransferFromFailed.selector); + echo.acceptOffer(offerId); + } } diff --git a/test/BaseTest.t.sol b/test/BaseTest.t.sol index b33e2bc..37bcaf2 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; @@ -38,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); @@ -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/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)}); + } +} 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/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/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; diff --git a/test/utils/OfferUtils.sol b/test/utils/OfferUtils.sol index 2b22558..aa61a21 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,66 @@ abstract contract OfferUtils is Test { ); } + // TODO Should receive an array of types + 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++) { + 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}); + } + 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.tokenType == TokenType.ERC20) { + ERC20 tokenContract = ERC20(item.tokenAddress); + assertEq(tokenContract.balanceOf(owner), item.tokenIdOrAmount); + } else { + Mocked721 tokenContract = Mocked721(item.tokenAddress); + assertEq(tokenContract.ownerOf(item.tokenIdOrAmount), owner); + } } } @@ -76,7 +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].tokenIdOrAmount, items2.items[i].tokenIdOrAmount); + if (items1.items[i].tokenType != items2.items[i].tokenType) { + assertTrue(false); + } } }