diff --git a/contracts/SwapKiwi.sol b/contracts/SwapKiwi.sol index 551ee5f..1a0f469 100644 --- a/contracts/SwapKiwi.sol +++ b/contracts/SwapKiwi.sol @@ -14,314 +14,395 @@ import "@openzeppelin/contracts/access/Ownable.sol"; */ contract SwapKiwi is Ownable, ERC721Holder, ERC1155Holder { - uint64 private _swapsCounter; - uint128 private _etherLocked; - uint128 public fee; - - mapping (uint64 => Swap) private _swaps; - - struct Swap { - address payable initiator; - address[] initiatorNftAddresses; - uint256[] initiatorNftIds; - uint256[] initiatorNftAmounts; - address payable secondUser; - address[] secondUserNftAddresses; - uint256[] secondUserNftIds; - uint256[] secondUserNftAmounts; - uint128 initiatorEtherValue; - uint128 secondUserEtherValue; - } - - event SwapExecuted(address indexed from, address indexed to, uint64 indexed swapId); - event SwapCanceled(address indexed canceledBy, uint64 indexed swapId); - event SwapProposed( - address indexed from, - address indexed to, - uint64 indexed swapId, - uint128 etherValue, - address[] nftAddresses, - uint256[] nftIds, - uint256[] nftAmounts - ); - event SwapInitiated( - address indexed from, - address indexed to, - uint64 indexed swapId, - uint128 etherValue, - address[] nftAddresses, - uint256[] nftIds, - uint256[] nftAmounts - ); - event AppFeeChanged( - uint128 fee - ); - - modifier onlyInitiator(uint64 swapId) { - require(msg.sender == _swaps[swapId].initiator, - "SwapKiwi: caller is not swap initiator"); - _; - } - - modifier requireSameLength(address[] memory nftAddresses, uint256[] memory nftIds, uint256[] memory nftAmounts) { - require(nftAddresses.length == nftIds.length, "SwapKiwi: NFT and ID arrays have to be same length"); - require(nftAddresses.length == nftAmounts.length, "SwapKiwi: NFT and AMOUNT arrays have to be same length"); - _; - } - - modifier chargeAppFee() { - require(msg.value >= fee, "SwapKiwi: Sent ETH amount needs to be more or equal application fee"); - _; - } - - constructor(uint128 initalAppFee, address contractOwnerAddress) { - fee = initalAppFee; - super.transferOwnership(contractOwnerAddress); - } - - function setAppFee(uint128 newFee) external onlyOwner { - fee = newFee; - emit AppFeeChanged(newFee); - } - - /** - * @dev First user proposes a swap to the second user with the NFTs that he deposits and wants to trade. - * Proposed NFTs are transfered to the SwapKiwi contract and - * kept there until the swap is accepted or canceled/rejected. - * - * @param secondUser address of the user that the first user wants to trade NFTs with - * @param nftAddresses array of NFT addressed that want to be traded - * @param nftIds array of IDs belonging to NFTs that want to be traded - * @param nftAmounts array of NFT amounts that want to be traded. If the amount is zero, that means - * the token is ERC721 token. Otherwise the token is ERC1155 token. - */ - function proposeSwap( - address secondUser, - address[] memory nftAddresses, - uint256[] memory nftIds, - uint256[] memory nftAmounts - ) external payable chargeAppFee requireSameLength(nftAddresses, nftIds, nftAmounts) { - _swapsCounter += 1; - - safeMultipleTransfersFrom( - msg.sender, - address(this), - nftAddresses, - nftIds, - nftAmounts - ); - - Swap storage swap = _swaps[_swapsCounter]; - swap.initiator = payable(msg.sender); - swap.initiatorNftAddresses = nftAddresses; - swap.initiatorNftIds = nftIds; - swap.initiatorNftAmounts = nftAmounts; - - uint128 _fee = fee; - - if (msg.value > _fee) { - swap.initiatorEtherValue = uint128(msg.value) - _fee; - _etherLocked += swap.initiatorEtherValue; - } - swap.secondUser = payable(secondUser); - - emit SwapProposed( - msg.sender, - secondUser, - _swapsCounter, - swap.initiatorEtherValue, - nftAddresses, - nftIds, - nftAmounts - ); - } - - /** - * @dev Second user accepts the swap (with proposed NFTs) from swap initiator and - * deposits his NFTs into the SwapKiwi contract. - * Callable only by second user that is invited by swap initiator. - * - * @param swapId ID of the swap that the second user is invited to participate in - * @param nftAddresses array of NFT addressed that want to be traded - * @param nftIds array of IDs belonging to NFTs that want to be traded - * @param nftAmounts array of NFT amounts that want to be traded. If the amount is zero, that means - * the token is ERC721 token. Otherwise the token is ERC1155 token. - */ - function initiateSwap( - uint64 swapId, - address[] memory nftAddresses, - uint256[] memory nftIds, - uint256[] memory nftAmounts - ) external payable chargeAppFee requireSameLength(nftAddresses, nftIds, nftAmounts) { - require(_swaps[swapId].secondUser == msg.sender, "SwapKiwi: caller is not swap participator"); - require( - _swaps[swapId].secondUserEtherValue == 0 && - ( _swaps[swapId].secondUserNftAddresses.length == 0 && - _swaps[swapId].secondUserNftIds.length == 0 && - _swaps[swapId].secondUserNftAmounts.length == 0 - ), "SwapKiwi: swap already initiated" - ); - - safeMultipleTransfersFrom( - msg.sender, - address(this), - nftAddresses, - nftIds, - nftAmounts - ); - - _swaps[swapId].secondUserNftAddresses = nftAddresses; - _swaps[swapId].secondUserNftIds = nftIds; - _swaps[swapId].secondUserNftAmounts = nftAmounts; - - uint128 _fee = fee; - - if (msg.value > _fee) { - _swaps[swapId].secondUserEtherValue = uint128(msg.value) - _fee; - _etherLocked += _swaps[swapId].secondUserEtherValue; - } - - emit SwapInitiated( - msg.sender, - _swaps[swapId].initiator, - swapId, - _swaps[swapId].secondUserEtherValue, - nftAddresses, - nftIds, - nftAmounts - ); - } - - /** - * @dev Swap initiator accepts the swap (NFTs proposed by the second user). - * Executeds the swap - transfers NFTs from SwapKiwi to the participating users. - * Callable only by swap initiator. - * - * @param swapId ID of the swap that the initator wants to execute - */ - function acceptSwap(uint64 swapId) external onlyInitiator(swapId) { - require( - (_swaps[swapId].secondUserNftAddresses.length != 0 || _swaps[swapId].secondUserEtherValue > 0) && - (_swaps[swapId].initiatorNftAddresses.length != 0 || _swaps[swapId].initiatorEtherValue > 0), - "SwapKiwi: Can't accept swap, both participants didn't add NFTs" - ); - - // transfer NFTs from escrow to initiator - safeMultipleTransfersFrom( - address(this), - _swaps[swapId].initiator, - _swaps[swapId].secondUserNftAddresses, - _swaps[swapId].secondUserNftIds, - _swaps[swapId].secondUserNftAmounts - ); - - // transfer NFTs from escrow to second user - safeMultipleTransfersFrom( - address(this), - _swaps[swapId].secondUser, - _swaps[swapId].initiatorNftAddresses, - _swaps[swapId].initiatorNftIds, - _swaps[swapId].initiatorNftAmounts - ); - - if (_swaps[swapId].initiatorEtherValue != 0) { - _etherLocked -= _swaps[swapId].initiatorEtherValue; - uint128 amountToTransfer = _swaps[swapId].initiatorEtherValue; - _swaps[swapId].initiatorEtherValue = 0; - _swaps[swapId].secondUser.transfer(amountToTransfer); - } - if (_swaps[swapId].secondUserEtherValue != 0) { - _etherLocked -= _swaps[swapId].secondUserEtherValue; - uint128 amountToTransfer = _swaps[swapId].secondUserEtherValue; - _swaps[swapId].secondUserEtherValue = 0; - _swaps[swapId].initiator.transfer(amountToTransfer); - } - - emit SwapExecuted(_swaps[swapId].initiator, _swaps[swapId].secondUser, swapId); - - delete _swaps[swapId]; - } - - /** - * @dev Returns NFTs from SwapKiwi to swap initator. - * Callable only if second user hasn't yet added NFTs. - * - * @param swapId ID of the swap that the swap participants want to cancel - */ - function cancelSwap(uint64 swapId) external { - require( - _swaps[swapId].initiator == msg.sender || _swaps[swapId].secondUser == msg.sender, - "SwapKiwi: Can't cancel swap, must be swap participant" - ); - // return initiator NFTs - safeMultipleTransfersFrom( - address(this), - _swaps[swapId].initiator, - _swaps[swapId].initiatorNftAddresses, - _swaps[swapId].initiatorNftIds, - _swaps[swapId].initiatorNftAmounts - ); - - if(_swaps[swapId].secondUserNftAddresses.length != 0) { - // return second user NFTs - safeMultipleTransfersFrom( - address(this), - _swaps[swapId].secondUser, - _swaps[swapId].secondUserNftAddresses, - _swaps[swapId].secondUserNftIds, - _swaps[swapId].secondUserNftAmounts - ); - } - - if (_swaps[swapId].initiatorEtherValue != 0) { - _etherLocked -= _swaps[swapId].initiatorEtherValue; - uint128 amountToTransfer = _swaps[swapId].initiatorEtherValue; - _swaps[swapId].initiatorEtherValue = 0; - _swaps[swapId].initiator.transfer(amountToTransfer); - } - if (_swaps[swapId].secondUserEtherValue != 0) { - _etherLocked -= _swaps[swapId].secondUserEtherValue; - uint128 amountToTransfer = _swaps[swapId].secondUserEtherValue; - _swaps[swapId].secondUserEtherValue = 0; - _swaps[swapId].secondUser.transfer(amountToTransfer); - } - - emit SwapCanceled(msg.sender, swapId); - - delete _swaps[swapId]; - } - - function safeMultipleTransfersFrom( - address from, - address to, - address[] memory nftAddresses, - uint256[] memory nftIds, - uint256[] memory nftAmounts - ) internal virtual { - for (uint256 i=0; i < nftIds.length; i++){ - safeTransferFrom(from, to, nftAddresses[i], nftIds[i], nftAmounts[i], ""); - } - } - - function safeTransferFrom( - address from, - address to, - address tokenAddress, - uint256 tokenId, - uint256 tokenAmount, - bytes memory _data - ) internal virtual { - if (tokenAmount == 0) { - IERC721(tokenAddress).safeTransferFrom(from, to, tokenId, _data); - } else { - IERC1155(tokenAddress).safeTransferFrom(from, to, tokenId, tokenAmount, _data); - } - - } - - function withdrawEther(address payable recipient) external onlyOwner { - require(recipient != address(0), "SwapKiwi: transfer to the zero address"); - - recipient.transfer((address(this).balance - _etherLocked)); - } + uint64 private _swapsCounter; + uint96 public etherLocked; + uint96 public fee; + + address private constant _ZEROADDRESS = address(0); + + mapping (uint64 => Swap) private _swaps; + + struct Swap { + address payable initiator; + uint96 initiatorEtherValue; + address[] initiatorNftAddresses; + uint256[] initiatorNftIds; + uint128[] initiatorNftAmounts; + address payable secondUser; + uint96 secondUserEtherValue; + address[] secondUserNftAddresses; + uint256[] secondUserNftIds; + uint128[] secondUserNftAmounts; + } + + event SwapExecuted(address indexed from, address indexed to, uint64 indexed swapId); + event SwapCanceled(address indexed canceledBy, uint64 indexed swapId); + event SwapCanceledWithSecondUserRevert(uint64 indexed swapId, bytes reason); + event SwapCanceledBySecondUser(uint64 indexed swapId); + event SwapProposed( + address indexed from, + address indexed to, + uint64 indexed swapId, + uint128 etherValue, + address[] nftAddresses, + uint256[] nftIds, + uint128[] nftAmounts + ); + event SwapInitiated( + address indexed from, + address indexed to, + uint64 indexed swapId, + uint128 etherValue, + address[] nftAddresses, + uint256[] nftIds, + uint128[] nftAmounts + ); + event AppFeeChanged( + uint96 fee + ); + event TransferEthToSecondUserFailed(uint64 indexed swapId); + + modifier onlyInitiator(uint64 swapId) { + require(msg.sender == _swaps[swapId].initiator, + "SwapKiwi: caller is not swap initiator"); + _; + } + + modifier onlySecondUser(uint64 swapId) { + require(msg.sender == _swaps[swapId].secondUser, + "SwapKiwi: caller is not swap secondUser"); + _; + } + + modifier onlyThisContractItself() { + require(msg.sender == address(this), "Invalid caller"); + _; + } + + modifier requireSameLength(address[] memory nftAddresses, uint256[] memory nftIds, uint128[] memory nftAmounts) { + require(nftAddresses.length == nftIds.length, "SwapKiwi: NFT and ID arrays have to be same length"); + require(nftAddresses.length == nftAmounts.length, "SwapKiwi: NFT and AMOUNT arrays have to be same length"); + _; + } + + modifier chargeAppFee() { + require(msg.value >= fee, "SwapKiwi: Sent ETH amount needs to be more or equal application fee"); + _; + } + + constructor(uint96 initalAppFee, address contractOwnerAddress) { + fee = initalAppFee; + super.transferOwnership(contractOwnerAddress); + } + + function setAppFee(uint96 newFee) external onlyOwner { + fee = newFee; + emit AppFeeChanged(newFee); + } + + /** + * @dev First user proposes a swap to the second user with the NFTs that he deposits and wants to trade. + * Proposed NFTs are transfered to the SwapKiwi contract and + * kept there until the swap is accepted or canceled/rejected. + * + * @param secondUser address of the user that the first user wants to trade NFTs with + * @param nftAddresses array of NFT addressed that want to be traded + * @param nftIds array of IDs belonging to NFTs that want to be traded + * @param nftAmounts array of NFT amounts that want to be traded. If the amount is zero, that means + * the token is ERC721 token. Otherwise the token is ERC1155 token. + */ + function proposeSwap( + address secondUser, + address[] memory nftAddresses, + uint256[] memory nftIds, + uint128[] memory nftAmounts + ) external payable chargeAppFee requireSameLength(nftAddresses, nftIds, nftAmounts) { + uint64 swapsCounter = _swapsCounter + 1; + _swapsCounter = swapsCounter; + + Swap storage swap = _swaps[swapsCounter]; + swap.initiator = payable(msg.sender); + + if(nftAddresses.length > 0) { + for (uint256 i = 0; i < nftIds.length; i++){ + safeTransferFrom(msg.sender, address(this), nftAddresses[i], nftIds[i], nftAmounts[i], ""); + } + + swap.initiatorNftAddresses = nftAddresses; + swap.initiatorNftIds = nftIds; + swap.initiatorNftAmounts = nftAmounts; + } + + uint96 _fee = fee; + uint96 initiatorEtherValue; + + if (msg.value > _fee) { + initiatorEtherValue = uint96(msg.value) - _fee; + swap.initiatorEtherValue = initiatorEtherValue; + etherLocked += initiatorEtherValue; + } + swap.secondUser = payable(secondUser); + + emit SwapProposed( + msg.sender, + secondUser, + swapsCounter, + initiatorEtherValue, + nftAddresses, + nftIds, + nftAmounts + ); + } + + /** + * @dev Second user accepts the swap (with proposed NFTs) from swap initiator and + * deposits his NFTs into the SwapKiwi contract. + * Callable only by second user that is invited by swap initiator. + * Even if the second user didn't provide any NFT and ether value equals to fee, it is considered valid. + * + * @param swapId ID of the swap that the second user is invited to participate in + * @param nftAddresses array of NFT addressed that want to be traded + * @param nftIds array of IDs belonging to NFTs that want to be traded + * @param nftAmounts array of NFT amounts that want to be traded. If the amount is zero, that means + * the token is ERC721 token. Otherwise the token is ERC1155 token. + */ + function initiateSwap( + uint64 swapId, + address[] memory nftAddresses, + uint256[] memory nftIds, + uint128[] memory nftAmounts + ) external payable chargeAppFee requireSameLength(nftAddresses, nftIds, nftAmounts) { + require(_swaps[swapId].secondUser == msg.sender, "SwapKiwi: caller is not swap participator"); + require( + _swaps[swapId].secondUserEtherValue == 0 && + _swaps[swapId].secondUserNftAddresses.length == 0 + , "SwapKiwi: swap already initiated" + ); + + if (nftAddresses.length > 0) { + for (uint256 i = 0; i < nftIds.length; i++){ + safeTransferFrom(msg.sender, address(this), nftAddresses[i], nftIds[i], nftAmounts[i], ""); + } + + _swaps[swapId].secondUserNftAddresses = nftAddresses; + _swaps[swapId].secondUserNftIds = nftIds; + _swaps[swapId].secondUserNftAmounts = nftAmounts; + } + + uint96 _fee = fee; + uint96 secondUserEtherValue; + + if (msg.value > _fee) { + secondUserEtherValue = uint96(msg.value) - _fee; + _swaps[swapId].secondUserEtherValue = secondUserEtherValue; + etherLocked += secondUserEtherValue; + } + + emit SwapInitiated( + msg.sender, + _swaps[swapId].initiator, + swapId, + secondUserEtherValue, + nftAddresses, + nftIds, + nftAmounts + ); + } + + /** + * @dev Swap initiator accepts the swap (NFTs proposed by the second user). + * Executeds the swap - transfers NFTs from SwapKiwi to the participating users. + * Callable only by swap initiator. + * + * @param swapId ID of the swap that the initator wants to execute + */ + function acceptSwap(uint64 swapId) external onlyInitiator(swapId) { + Swap memory swap = _swaps[swapId]; + delete _swaps[swapId]; + + require( + (swap.secondUserNftAddresses.length > 0 || swap.secondUserEtherValue > 0) && + (swap.initiatorNftAddresses.length > 0 || swap.initiatorEtherValue > 0), + "SwapKiwi: Can't accept swap, both participants didn't add NFTs" + ); + + if (swap.secondUserNftAddresses.length > 0) { + // transfer NFTs from escrow to initiator + for (uint256 i = 0; i < swap.secondUserNftIds.length; i++) { + safeTransferFrom( + address(this), + swap.initiator, + swap.secondUserNftAddresses[i], + swap.secondUserNftIds[i], + swap.secondUserNftAmounts[i], + "" + ); + } + } + + if (swap.initiatorNftAddresses.length > 0) { + // transfer NFTs from escrow to second user + for (uint256 i = 0; i < swap.initiatorNftIds.length; i++) { + safeTransferFrom( + address(this), + swap.secondUser, + swap.initiatorNftAddresses[i], + swap.initiatorNftIds[i], + swap.initiatorNftAmounts[i], + "" + ); + } + } + + if (swap.initiatorEtherValue > 0) { + etherLocked -= swap.initiatorEtherValue; + (bool success,) = swap.secondUser.call{value: swap.initiatorEtherValue}(""); + require(success, "Failed to send Ether to the second user"); + } + if (swap.secondUserEtherValue > 0) { + etherLocked -= swap.secondUserEtherValue; + (bool success,) = swap.initiator.call{value: swap.secondUserEtherValue}(""); + require(success, "Failed to send Ether to the initiator user"); + } + + emit SwapExecuted(swap.initiator, swap.secondUser, swapId); + } + + /** + * @dev Returns NFTs from SwapKiwi to swap initator. + * Callable only if second user hasn't yet added NFTs. + * + * @param swapId ID of the swap that the swap participants want to cancel + */ + function cancelSwap(uint64 swapId) external returns (bool) { + Swap memory swap = _swaps[swapId]; + delete _swaps[swapId]; + + require( + swap.initiator == msg.sender || swap.secondUser == msg.sender, + "SwapKiwi: Can't cancel swap, must be swap participant" + ); + + if (swap.initiatorNftAddresses.length > 0) { + // return initiator NFTs + for (uint256 i = 0; i < swap.initiatorNftIds.length; i++) { + safeTransferFrom( + address(this), + swap.initiator, + swap.initiatorNftAddresses[i], + swap.initiatorNftIds[i], + swap.initiatorNftAmounts[i], + "" + ); + } + } + + if (swap.initiatorEtherValue != 0) { + etherLocked -= swap.initiatorEtherValue; + (bool success,) = swap.initiator.call{value: swap.initiatorEtherValue}(""); + require(success, "Failed to send Ether to the initiator user"); + } + + if(swap.secondUserNftAddresses.length > 0) { + // return second user NFTs + try this.safeMultipleTransfersFrom( + address(this), + swap.secondUser, + swap.secondUserNftAddresses, + swap.secondUserNftIds, + swap.secondUserNftAmounts + ) {} catch (bytes memory reason) { + _swaps[swapId].secondUser = swap.secondUser; + _swaps[swapId].secondUserNftAddresses = swap.secondUserNftAddresses; + _swaps[swapId].secondUserNftIds = swap.secondUserNftIds; + _swaps[swapId].secondUserNftAmounts = swap.secondUserNftAmounts; + _swaps[swapId].secondUserEtherValue = swap.secondUserEtherValue; + emit SwapCanceledWithSecondUserRevert(swapId, reason); + return true; + } + } + + if (swap.secondUserEtherValue != 0) { + etherLocked -= swap.secondUserEtherValue; + (bool success,) = swap.secondUser.call{value: swap.secondUserEtherValue}(""); + if (!success) { + etherLocked += swap.secondUserEtherValue; + _swaps[swapId].secondUser = swap.secondUser; + _swaps[swapId].secondUserEtherValue = swap.secondUserEtherValue; + emit TransferEthToSecondUserFailed(swapId); + return true; + } + } + + emit SwapCanceled(msg.sender, swapId); + return true; + } + + function cancelSwapBySecondUser(uint64 swapId) external onlySecondUser(swapId) { + Swap memory swap = _swaps[swapId]; + delete _swaps[swapId]; + + if(swap.secondUserNftAddresses.length > 0) { + // return second user NFTs + for (uint256 i = 0; i < swap.secondUserNftIds.length; i++) { + safeTransferFrom( + address(this), + swap.secondUser, + swap.secondUserNftAddresses[i], + swap.secondUserNftIds[i], + swap.secondUserNftAmounts[i], + "" + ); + } + } + + if (swap.secondUserEtherValue != 0) { + etherLocked -= swap.secondUserEtherValue; + (bool success,) = swap.secondUser.call{value: swap.secondUserEtherValue}(""); + require(success, "Failed to send Ether to the second user"); + } + + if (swap.initiator != _ZEROADDRESS) { + _swaps[swapId].initiator = swap.initiator; + _swaps[swapId].initiatorEtherValue = swap.initiatorEtherValue; + _swaps[swapId].initiatorNftAddresses = swap.initiatorNftAddresses; + _swaps[swapId].initiatorNftIds = swap.initiatorNftIds; + _swaps[swapId].initiatorNftAmounts = swap.initiatorNftAmounts; + } + + emit SwapCanceledBySecondUser(swapId); + } + + function safeMultipleTransfersFrom( + address from, + address to, + address[] memory nftAddresses, + uint256[] memory nftIds, + uint128[] memory nftAmounts + ) external onlyThisContractItself { + for (uint256 i = 0; i < nftIds.length; i++) { + safeTransferFrom(from, to, nftAddresses[i], nftIds[i], nftAmounts[i], ""); + } + } + + function safeTransferFrom( + address from, + address to, + address tokenAddress, + uint256 tokenId, + uint256 tokenAmount, + bytes memory _data + ) internal virtual { + if (tokenAmount == 0) { + IERC721(tokenAddress).transferFrom(from, to, tokenId); + } else { + IERC1155(tokenAddress).safeTransferFrom(from, to, tokenId, tokenAmount, _data); + } + } + + function withdrawEther(address payable recipient) external onlyOwner { + require(recipient != address(0), "SwapKiwi: transfer to the zero address"); + + recipient.transfer((address(this).balance - etherLocked)); + } } diff --git a/contracts/Test.sol b/contracts/Test.sol new file mode 100644 index 0000000..4188724 --- /dev/null +++ b/contracts/Test.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.1; + +import "hardhat/console.sol"; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; +import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; + +interface ISwapKiwi { + function proposeSwap( + address secondUser, + address[] memory nftAddresses, + uint256[] memory nftIds, + uint128[] memory nftAmounts + ) external payable; + + function initiateSwap( + uint64 swapId, + address[] memory nftAddresses, + uint256[] memory nftIds, + uint128[] memory nftAmounts + ) external payable; + + function cancelSwap(uint64 swapId) external; + function cancelSwapByInitiator(uint64 swapId) external; + function cancelSwapBySecondUser(uint64 swapId) external; +} + +contract TestERC721 is ERC721("TEST", "TEST") { + + function mint(address account, uint256 tokenId) public { + _mint(account, tokenId); + } + + receive() external payable {} + +} + +contract TestERC1155 is ERC1155("TEST") { + function mint(address account, uint256 tokenId, uint256 tokenAmount) public { + _mint(account, tokenId, tokenAmount, ""); + } +} + +contract SwapParticipant { + address public swapContract; + uint public counter; + uint public counter2; + + event Received(address indexed sender, uint amount); + + function proposeSwap( + address secondUser, + address[] memory nftAddresses, + uint256[] memory nftIds, + uint128[] memory nftAmounts + ) external payable { + for (uint256 i = 0; i < nftAddresses.length; i++) { + IERC1155(nftAddresses[i]).setApprovalForAll(swapContract, true); + } + ISwapKiwi(swapContract).proposeSwap{ value: msg.value }(secondUser, nftAddresses, nftIds, nftAmounts); + } + + function initiateSwap( + uint64 swapId, + address[] memory nftAddresses, + uint256[] memory nftIds, + uint128[] memory nftAmounts + ) external payable { + for (uint256 i = 0; i < nftAddresses.length; i++) { + IERC1155(nftAddresses[i]).setApprovalForAll(swapContract, true); + } + ISwapKiwi(swapContract).initiateSwap{ value: msg.value }(swapId, nftAddresses, nftIds, nftAmounts); + } + + function cancelSwap(uint64 swapId) external { + ISwapKiwi(swapContract).cancelSwap(swapId); + } + + function cancelSwapByInitiator(uint64 swapId) external { + uint balanceBefore = address(this).balance; + ISwapKiwi(swapContract).cancelSwapByInitiator(swapId); + uint balanceAfter = address(this).balance; + if (balanceAfter > balanceBefore) { + (bool success,) = payable(msg.sender).call{value: balanceAfter - balanceBefore}(""); + require(success, "Failed to send Ether to the initiator user"); + } + } + + function cancelSwapBySecondUser(uint64 swapId) external { + uint balanceBefore = address(this).balance; + ISwapKiwi(swapContract).cancelSwapBySecondUser(swapId); + uint balanceAfter = address(this).balance; + if (balanceAfter > balanceBefore) { + (bool success,) = payable(msg.sender).call{value: balanceAfter - balanceBefore}(""); + require(success, "Failed to send Ether to the initiator user"); + } + } + + function setCounter(uint _counter) external { + counter = _counter; + } + + function setCounter2(uint _counter) external { + counter2 = _counter; + } + + function setSwap(address _swapAddress) external { + swapContract = _swapAddress; + } + + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes memory + ) public returns (bytes4) { + if (counter != 0) { + revert("The malicious onERC1155Received contract"); + } + return this.onERC1155Received.selector; + } + + receive() external payable { + if (counter2 != 0) { + revert("eth transfer reverted"); + } + emit Received(msg.sender, msg.value); + } +} diff --git a/contracts/Test_NFT.sol b/contracts/Test_NFT.sol deleted file mode 100644 index 24daac8..0000000 --- a/contracts/Test_NFT.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.1; - -import "hardhat/console.sol"; -import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; - -contract TestERC721 is ERC721("TEST", "TEST") { - - function mint(address account, uint256 tokenId) public { - _mint(account, tokenId); - } - - receive() external payable {} - -} - -contract TestERC1155 is ERC1155("TEST") { - function mint(address account, uint256 tokenId, uint256 tokenAmount) public { - _mint(account, tokenId, tokenAmount, ""); - } -} diff --git a/scripts/0099_Deploy_TestNFT.ts b/scripts/0099_Deploy_TestNFT.ts index 17461ac..d016258 100644 --- a/scripts/0099_Deploy_TestNFT.ts +++ b/scripts/0099_Deploy_TestNFT.ts @@ -8,6 +8,7 @@ const deployFunc: DeployFunction = async function (hre: HardhatRuntimeEnvironmen await deploy("TestERC721", {from: deployer}); await deploy("TestERC1155", {from: deployer}); + await deploy("SwapParticipant", {from: deployer}); } // skip deployment if deploying to mainnet diff --git a/test/swapKiwi.test.ts b/test/swapKiwi.test.ts index 7540da0..5464148 100644 --- a/test/swapKiwi.test.ts +++ b/test/swapKiwi.test.ts @@ -4,6 +4,7 @@ import { ethers, deployments } from "hardhat"; import { SwapKiwi } from "../typechain/SwapKiwi"; import { TestERC721 } from "../typechain/TestERC721"; import { TestERC1155 } from "../typechain/TestERC1155"; +import { SwapParticipant } from "../typechain/SwapParticipant"; import { Signer } from "ethers"; import { TransactionReceipt } from "@ethersproject/providers"; import chaiAsPromised from 'chai-as-promised'; @@ -20,6 +21,9 @@ describe("SwapKiwi", async function () { let TestERC1155: Deployment; let appUserERC1155: TestERC1155; let otherAppUserERC1155: TestERC1155; + let SwapParticipant: Deployment; + let initiatorParticipant: SwapParticipant; + let secondUserParticipant: SwapParticipant; let signers: Signer[]; let appUser: SwapKiwi; let otherAppUser: SwapKiwi; @@ -29,7 +33,7 @@ describe("SwapKiwi", async function () { before(async () => { signers = await ethers.getSigners(); - ({ SwapKiwi, TestERC721, TestERC1155 } = await deployments.fixture()); + ({ SwapKiwi, TestERC721, TestERC1155, SwapParticipant } = await deployments.fixture()); swapKiwi = await ethers.getContractAt(SwapKiwi.abi, SwapKiwi.address, signers[0]) as SwapKiwi; @@ -39,6 +43,9 @@ describe("SwapKiwi", async function () { appUserERC1155 = await ethers.getContractAt(TestERC1155.abi, TestERC1155.address, signers[2]) as TestERC1155; otherAppUserERC1155 = await ethers.getContractAt(TestERC1155.abi, TestERC1155.address, signers[3]) as TestERC1155; + initiatorParticipant = await ethers.getContractAt(SwapParticipant.abi, SwapParticipant.address, signers[2]) as SwapParticipant; + secondUserParticipant = await ethers.getContractAt(SwapParticipant.abi, SwapParticipant.address, signers[3]) as SwapParticipant; + appUser = new ethers.Contract(swapKiwi.address, SwapKiwi.abi, signers[2]) as SwapKiwi; otherAppUser = new ethers.Contract(swapKiwi.address, SwapKiwi.abi, signers[3]) as SwapKiwi; appUserAddress = await signers[2].getAddress(); @@ -907,4 +914,182 @@ describe("SwapKiwi", async function () { await expect(swapKiwi.withdrawEther("0x0000000000000000000000000000000000000000")) .to.be.rejectedWith("SwapKiwi: transfer to the zero address"); }); + + it("The initiator should get their parts even if the common cancelling(part) is failed. And then the second user can get its part through a single side cancel", async function () { + const tokenIds = [1827, 1828]; + const tokenAmounts = [10, 20]; + + const secondUserBalance = await secondUserParticipant.signer.getBalance(); + + await appUserERC1155.mint(appUserAddress, tokenIds[0], tokenAmounts[0]); + await otherAppUserERC1155.mint(secondUserParticipant.address, tokenIds[1], tokenAmounts[1]); + + await appUserERC1155.setApprovalForAll(swapKiwi.address, true); + await secondUserParticipant.setSwap(swapKiwi.address); + + const tx = await appUser.proposeSwap(secondUserParticipant.address, [appUserERC1155.address], [tokenIds[0]], [tokenAmounts[0]], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + await secondUserParticipant.initiateSwap(swapIdFromLogs, [otherAppUserERC1155.address], [tokenIds[1]], [tokenAmounts[1]], { + value: VALID_APP_FEE.add(parseEther("50")) + }); + + await secondUserParticipant.setCounter(10); + + await appUser.cancelSwap(swapIdFromLogs); + + const initiator_erc1155BalanceAfterCancel = await appUserERC1155.balanceOf(appUserAddress, tokenIds[0]); + // check that ERC721 and ERC1155 are returned to initial owner + expect(initiator_erc1155BalanceAfterCancel.toNumber()).to.be.deep.equal(tokenAmounts[0]); + + await secondUserParticipant.setCounter(0); + await secondUserParticipant.cancelSwapBySecondUser(swapIdFromLogs); + const secondUser_erc1155BalanceAfterCancel = await otherAppUserERC1155.balanceOf(secondUserParticipant.address, tokenIds[1]); + expect(secondUser_erc1155BalanceAfterCancel.toNumber()).to.be.deep.equal(tokenAmounts[1]); + expect(secondUserBalance.sub(await secondUserParticipant.signer.getBalance()).lt(parseEther("1"))).to.be.equal(true); + }); + + it("The initiator should get their part even if the common cancelling(full) is failed. And then the second user can get its part through a single side cancel", async function () { + const tokenIds = [11827, 11828]; + const tokenAmounts = [10, 20]; + + const secondUserBalance = await secondUserParticipant.signer.getBalance(); + + await appUserERC1155.mint(initiatorParticipant.address, tokenIds[0], tokenAmounts[0]); + await otherAppUserERC1155.mint(secondUserParticipant.address, tokenIds[1], tokenAmounts[1]); + + await initiatorParticipant.setSwap(swapKiwi.address); + await secondUserParticipant.setSwap(swapKiwi.address); + + const tx = await initiatorParticipant.proposeSwap(secondUserParticipant.address, [appUserERC1155.address], [tokenIds[0]], [tokenAmounts[0]], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + await secondUserParticipant.initiateSwap(swapIdFromLogs, [otherAppUserERC1155.address], [tokenIds[1]], [tokenAmounts[1]], { + value: VALID_APP_FEE.add(parseEther("50")) + }); + + await initiatorParticipant.setCounter(10); + await expect(initiatorParticipant.cancelSwap(swapIdFromLogs)) + .to.be.rejectedWith("The malicious onERC1155Received contract"); + + const initiator_erc1155BalanceAfterCancel = await appUserERC1155.balanceOf(initiatorParticipant.address, tokenIds[0]); + // check that ERC721 and ERC1155 are returned to initial owner + expect(initiator_erc1155BalanceAfterCancel.toNumber()).to.be.deep.equal(0); + + await secondUserParticipant.setCounter(0); + await secondUserParticipant.cancelSwapBySecondUser(swapIdFromLogs); + const secondUser_erc1155BalanceAfterCancel = await otherAppUserERC1155.balanceOf(secondUserParticipant.address, tokenIds[1]); + expect(secondUser_erc1155BalanceAfterCancel.toNumber()).to.be.deep.equal(tokenAmounts[1]); + expect(secondUserBalance.sub(await secondUserParticipant.signer.getBalance()).lt(parseEther("1"))).to.be.equal(true); + }); + + it("The second user can get its part through a single side cancel. After that, the initiator can get its part through the common cancel", async function () { + const tokenIds = [1227, 1228]; + const tokenAmounts = [10, 20]; + + const secondUserBalance = await secondUserParticipant.signer.getBalance(); + + await appUserERC1155.mint(appUserAddress, tokenIds[0], tokenAmounts[0]); + await otherAppUserERC1155.mint(secondUserParticipant.address, tokenIds[1], tokenAmounts[1]); + + await appUserERC1155.setApprovalForAll(swapKiwi.address, true); + await secondUserParticipant.setSwap(swapKiwi.address); + + const tx = await appUser.proposeSwap(secondUserParticipant.address, [appUserERC1155.address], [tokenIds[0]], [tokenAmounts[0]], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + await secondUserParticipant.initiateSwap(swapIdFromLogs, [otherAppUserERC1155.address], [tokenIds[1]], [tokenAmounts[1]], { + value: VALID_APP_FEE.add(parseEther("50")) + }); + + await secondUserParticipant.cancelSwapBySecondUser(swapIdFromLogs); + const secondUser_erc1155BalanceAfterCancel = await otherAppUserERC1155.balanceOf(secondUserParticipant.address, tokenIds[1]); + expect(secondUser_erc1155BalanceAfterCancel.toNumber()).to.be.deep.equal(tokenAmounts[1]); + expect(secondUserBalance.sub(await secondUserParticipant.signer.getBalance()).lt(parseEther("1"))).to.be.equal(true); + + await appUser.cancelSwap(swapIdFromLogs); + const initiator_erc1155BalanceAfterCancel = await appUserERC1155.balanceOf(appUserAddress, tokenIds[0]); + // check that ERC721 and ERC1155 are returned to initial owner + expect(initiator_erc1155BalanceAfterCancel.toNumber()).to.be.deep.equal(tokenAmounts[0]); + }); + + it("After the second user does a single side cancel, the swap should be failed", async function () { + const tokenIds = [1027, 1028]; + const tokenAmounts = [10, 20]; + + const secondUserBalance = await secondUserParticipant.signer.getBalance(); + + await appUserERC1155.mint(appUserAddress, tokenIds[0], tokenAmounts[0]); + await otherAppUserERC1155.mint(secondUserParticipant.address, tokenIds[1], tokenAmounts[1]); + + await appUserERC1155.setApprovalForAll(swapKiwi.address, true); + await secondUserParticipant.setSwap(swapKiwi.address); + + const tx = await appUser.proposeSwap(secondUserParticipant.address, [appUserERC1155.address], [tokenIds[0]], [tokenAmounts[0]], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + await secondUserParticipant.initiateSwap(swapIdFromLogs, [otherAppUserERC1155.address], [tokenIds[1]], [tokenAmounts[1]], { + value: VALID_APP_FEE.add(parseEther("50")) + }); + + await secondUserParticipant.cancelSwapBySecondUser(swapIdFromLogs); + const secondUser_erc1155BalanceAfterCancel = await otherAppUserERC1155.balanceOf(secondUserParticipant.address, tokenIds[1]); + expect(secondUser_erc1155BalanceAfterCancel.toNumber()).to.be.deep.equal(tokenAmounts[1]); + expect(secondUserBalance.sub(await secondUserParticipant.signer.getBalance()).lt(parseEther("1"))).to.be.equal(true); + + await expect(appUser.acceptSwap(swapIdFromLogs)) + .to.be.rejectedWith("SwapKiwi: Can't accept swap, both participants didn't add NFTs"); + }); + + it("etherLocked amount should not be changed if eth transfer to the second user is failed when common cancel", async function () { + const tokenIds = [18827, 18828]; + const tokenAmounts = [10, 20]; + + const secondUserBalance = await secondUserParticipant.signer.getBalance(); + + await appUserERC1155.mint(appUserAddress, tokenIds[0], tokenAmounts[0]); + await otherAppUserERC1155.mint(secondUserParticipant.address, tokenIds[1], tokenAmounts[1]); + + await appUserERC1155.setApprovalForAll(swapKiwi.address, true); + await secondUserParticipant.setSwap(swapKiwi.address); + + const tx = await appUser.proposeSwap(secondUserParticipant.address, [appUserERC1155.address], [tokenIds[0]], [tokenAmounts[0]], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + await secondUserParticipant.initiateSwap(swapIdFromLogs, [otherAppUserERC1155.address], [tokenIds[1]], [tokenAmounts[1]], { + value: VALID_APP_FEE.add(parseEther("50")) + }); + + const locked = await appUser.etherLocked(); + + await secondUserParticipant.setCounter2(10); + await appUser.cancelSwap(swapIdFromLogs); + + const lockedAfterFailed = await appUser.etherLocked(); + + expect(lockedAfterFailed.toString()).to.be.deep.equal(locked.toString()); + const secondUser_erc1155BalanceAfterCancel = await otherAppUserERC1155.balanceOf(secondUserParticipant.address, tokenIds[1]); + expect(secondUser_erc1155BalanceAfterCancel.toNumber()).to.be.deep.equal(tokenAmounts[1]); + expect(secondUserBalance.sub(await secondUserParticipant.signer.getBalance()).gt(parseEther("50"))).to.be.equal(true); + }); }); diff --git a/test/swapKiwiWithOnlyERC721.test.ts b/test/swapKiwiWithOnlyERC721.test.ts new file mode 100644 index 0000000..892816a --- /dev/null +++ b/test/swapKiwiWithOnlyERC721.test.ts @@ -0,0 +1,678 @@ +import { Deployment } from "hardhat-deploy/types"; +import { expect, use } from "chai"; +import { ethers, deployments } from "hardhat"; +import { SwapKiwi } from "../typechain/SwapKiwi"; +import { TestERC721 } from "../typechain/TestERC721"; +import { Signer } from "ethers"; +import { TransactionReceipt } from "@ethersproject/providers"; +import chaiAsPromised from 'chai-as-promised'; +import { parseEther } from "ethers/lib/utils"; + +use(chaiAsPromised); + +describe("SwapKiwi-With only ERC721 to compare SwapKiwiV1", async function () { + let SwapKiwi: Deployment; + let swapKiwi: SwapKiwi; + let TestERC721: Deployment; + let appUserNFT: TestERC721; + let otherAppUserNFT: TestERC721; + let signers: Signer[]; + let appUser: SwapKiwi; + let otherAppUser: SwapKiwi; + let appUserAddress: string; + let otherAppUserAddress: string; + const VALID_APP_FEE = ethers.utils.parseEther("0.1"); + + before(async () => { + signers = await ethers.getSigners(); + ({ SwapKiwi, TestERC721 } = await deployments.fixture()); + swapKiwi = await ethers.getContractAt(SwapKiwi.abi, SwapKiwi.address, signers[0]) as SwapKiwi; + + appUserNFT = await ethers.getContractAt(TestERC721.abi, TestERC721.address, signers[2]) as TestERC721; + otherAppUserNFT = await ethers.getContractAt(TestERC721.abi, TestERC721.address, signers[3]) as TestERC721; + + appUser = new ethers.Contract(swapKiwi.address, SwapKiwi.abi, signers[2]) as SwapKiwi; + otherAppUser = new ethers.Contract(swapKiwi.address, SwapKiwi.abi, signers[3]) as SwapKiwi; + appUserAddress = await signers[2].getAddress(); + otherAppUserAddress = await signers[3].getAddress(); + }); + + function getFilterName(eventName: string) { + let filter: any; + switch (eventName) { + case "SwapExecuted": + filter = swapKiwi.filters.SwapExecuted(null, null, null); + break; + case "SwapCanceled": + filter = swapKiwi.filters.SwapCanceled(null, null); + break; + case "SwapProposed": + filter = swapKiwi.filters.SwapProposed(null, null, null, null, null, null, null); + break; + case "SwapInitiated": + filter = swapKiwi.filters.SwapInitiated(null, null, null, null, null, null, null); + default: null + } + return filter; + } + + async function getEventWithArgsFromLogs(txReceipt: TransactionReceipt, eventName: string): Promise { + if (txReceipt.logs) { + const events = await swapKiwi.queryFilter(getFilterName(eventName), undefined); + return events.map((e) => { + if (e.event == eventName) { + return { + eventName: eventName, + args: e.args + } + } + } + ).pop() + } + return null; + } + + it("Should successfuly set app fee if caller is the owner", async function () { + await swapKiwi.setAppFee(VALID_APP_FEE); + expect((await swapKiwi.fee()).toString()).to.be.deep.equal(VALID_APP_FEE.toString()); + }); + + it("Should fail to set app fee if caller is not owner", async function () { + const nonOwnerContract = new ethers.Contract(SwapKiwi.address, SwapKiwi.abi, signers[6]) as SwapKiwi; + + await expect(nonOwnerContract.setAppFee(1000)) + .to.be.rejectedWith("Ownable: caller is not the owner"); + }); + + it('Should fail to propose swap with invalid app fee', async function () { + await expect(appUser.proposeSwap(otherAppUserAddress, [], [], [], { + value: parseEther("0.01") + })).to.be.rejectedWith( + "SwapKiwi: Sent ETH amount needs to be more or equal application fee" + ); + }); + + it('Should fail to propose swap with different nft address and if length', async function () { + await expect(appUser.proposeSwap(otherAppUserAddress, [], [13], [], { + value: VALID_APP_FEE + })).to.be.rejectedWith( + "SwapKiwi: NFT and ID arrays have to be same length" + ); + }); + + it('Should succesfully deposit NFT into escrow contract and emit "SwapProposed" event', async function () { + await appUserNFT.mint(appUserAddress, 25); + await appUserNFT.approve(swapKiwi.address, 25); + expect(await appUserNFT.ownerOf(25)).to.be.deep.equal(appUserAddress); + + const tx = await appUser.proposeSwap(otherAppUserAddress, [appUserNFT.address], [25], [0], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + + // check if all values are emitted in event + expect(logs.eventName).to.be.deep.equal("SwapProposed"); + expect(logs.args.from).to.be.deep.equal(appUserAddress); + expect(logs.args.to).to.be.deep.equal(otherAppUserAddress); + expect(logs.args.nftAddresses[0]).to.be.deep.equal(appUserNFT.address); + expect(logs.args.nftIds[0].toString()).to.be.deep.equal("25"); + expect(await appUserNFT.ownerOf(25)).to.be.deep.equal(swapKiwi.address); + }); + + it('Should succesfully cancel swap by first user (after swap proposed) and emit "SwapCanceled" event', async function () { + await appUserNFT.mint(appUserAddress, 140); + await appUserNFT.approve(swapKiwi.address, 140); + const tx = await appUser.proposeSwap(otherAppUserAddress, [appUserNFT.address], [140], [0], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + const cancelTx = await appUser.cancelSwap(swapIdFromLogs); + const cancelTxReceipt = await cancelTx.wait(1); + const cancelTxlogs = await getEventWithArgsFromLogs(cancelTxReceipt, "SwapCanceled"); + + // check if all values are emitted in event + expect(cancelTxlogs.eventName).to.be.deep.equal("SwapCanceled"); + expect(cancelTxlogs.args.canceledBy).to.be.deep.equal(appUserAddress); + // expect that swap ID from "SwapCanceled" is same as swap ID from "swapProposed" event + expect(cancelTxlogs.args.swapId.toString()).to.be.deep.equal(String(swapIdFromLogs)); + // check that NFT is returned to initial owner + expect(await appUserNFT.ownerOf(140)).to.be.deep.equal(appUserAddress); + }); + + it('Should succesfully cancel swap by second user (after swap proposed) and emit "SwapCanceled" event', async function () { + await appUserNFT.mint(appUserAddress, 141); + await appUserNFT.approve(swapKiwi.address, 141); + const tx = await appUser.proposeSwap(otherAppUserAddress, [appUserNFT.address], [141], [0], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + const cancelTx = await otherAppUser.cancelSwap(swapIdFromLogs); + const cancelTxReceipt = await cancelTx.wait(1); + const cancelTxlogs = await getEventWithArgsFromLogs(cancelTxReceipt, "SwapCanceled"); + + // check if all values are emitted in event + expect(cancelTxlogs.eventName).to.be.deep.equal("SwapCanceled"); + expect(cancelTxlogs.args.canceledBy).to.be.deep.equal(otherAppUserAddress); + // expect that swap ID from "SwapCanceled" is same as swap ID from "swapProposed" event + expect(cancelTxlogs.args.swapId.toString()).to.be.deep.equal(String(swapIdFromLogs)); + // check that NFT is returned to initial owner + expect(await appUserNFT.ownerOf(141)).to.be.deep.equal(appUserAddress); + }); + + it('Should succesfully cancel swap by first user (after swap initiated) and emit "SwapCanceled" event', async function () { + await appUserNFT.mint(appUserAddress, 120); + await appUserNFT.approve(swapKiwi.address, 120); + const tx = await appUser.proposeSwap(otherAppUserAddress, [appUserNFT.address], [120], [0], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + await otherAppUserNFT.mint(otherAppUserAddress, 130); + await otherAppUserNFT.mint(otherAppUserAddress, 131); + await otherAppUserNFT.approve(swapKiwi.address, 130); + await otherAppUserNFT.approve(swapKiwi.address, 131); + const initiateSwapTx = await otherAppUser.initiateSwap( + swapIdFromLogs, + [otherAppUserNFT.address, otherAppUserNFT.address], + [130, 131], + [0, 0], + { + value: VALID_APP_FEE + } + ); + const initiateSwapTxReceipt = await initiateSwapTx.wait(1); + const initiateSwapLogs = await getEventWithArgsFromLogs(initiateSwapTxReceipt, "SwapInitiated"); + // check if all values are emitted in "SwapInitiated" event + expect(initiateSwapLogs.eventName).to.be.deep.equal("SwapInitiated"); + expect(initiateSwapLogs.args.from).to.be.deep.equal(otherAppUserAddress); + expect(initiateSwapLogs.args.to).to.be.deep.equal(appUserAddress); + + const cancelTx = await otherAppUser.cancelSwap(swapIdFromLogs); + const cancelTxReceipt = await cancelTx.wait(1); + const cancelTxlogs = await getEventWithArgsFromLogs(cancelTxReceipt, "SwapCanceled"); + + // check if all values are emitted in event + expect(cancelTxlogs.eventName).to.be.deep.equal("SwapCanceled"); + expect(cancelTxlogs.args.canceledBy).to.be.deep.equal(otherAppUserAddress); + // expect that swap ID from "SwapCanceled" is same as swap ID from "swapProposed" event + expect(cancelTxlogs.args.swapId.toString()).to.be.deep.equal(String(swapIdFromLogs)); + // check that NFT is returned to initial owners + expect(await appUserNFT.ownerOf(120)).to.be.deep.equal(appUserAddress); + expect(await appUserNFT.ownerOf(130)).to.be.deep.equal(otherAppUserAddress); + expect(await appUserNFT.ownerOf(131)).to.be.deep.equal(otherAppUserAddress); + }); + + it('Should succesfully cancel swap by second user (after swap initiated) and emit "SwapCanceled" event', async function () { + await appUserNFT.mint(appUserAddress, 121); + await appUserNFT.approve(swapKiwi.address, 121); + const tx = await appUser.proposeSwap(otherAppUserAddress, [appUserNFT.address], [121], [0], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + await otherAppUserNFT.mint(otherAppUserAddress, 135); + await otherAppUserNFT.mint(otherAppUserAddress, 136); + await otherAppUserNFT.approve(swapKiwi.address, 135); + await otherAppUserNFT.approve(swapKiwi.address, 136); + const initiateSwapTx = await otherAppUser.initiateSwap( + swapIdFromLogs, + [otherAppUserNFT.address, otherAppUserNFT.address], + [135, 136], + [0, 0], + { + value: VALID_APP_FEE + } + ); + const initiateSwapTxReceipt = await initiateSwapTx.wait(1); + const initiateSwapLogs = await getEventWithArgsFromLogs(initiateSwapTxReceipt, "SwapInitiated"); + // check if all values are emitted in "SwapInitiated" event + expect(initiateSwapLogs.eventName).to.be.deep.equal("SwapInitiated"); + expect(initiateSwapLogs.args.from).to.be.deep.equal(otherAppUserAddress); + expect(initiateSwapLogs.args.to).to.be.deep.equal(appUserAddress); + + const cancelTx = await otherAppUser.cancelSwap(swapIdFromLogs); + const cancelTxReceipt = await cancelTx.wait(1); + const cancelTxlogs = await getEventWithArgsFromLogs(cancelTxReceipt, "SwapCanceled"); + + // check if all values are emitted in event + expect(cancelTxlogs.eventName).to.be.deep.equal("SwapCanceled"); + expect(cancelTxlogs.args.canceledBy).to.be.deep.equal(otherAppUserAddress); + // expect that swap ID from "SwapCanceled" is same as swap ID from "swapProposed" event + expect(cancelTxlogs.args.swapId.toString()).to.be.deep.equal(String(swapIdFromLogs)); + // check that NFT is returned to initial owners + expect(await appUserNFT.ownerOf(121)).to.be.deep.equal(appUserAddress); + expect(await appUserNFT.ownerOf(135)).to.be.deep.equal(otherAppUserAddress); + expect(await appUserNFT.ownerOf(136)).to.be.deep.equal(otherAppUserAddress); + }); + + it('Should succesfully cancel swap created with ether value', async function () { + const firstUserBalance = await appUser.signer.getBalance(); + const secondUserBalance = await otherAppUser.signer.getBalance(); + + await appUserNFT.mint(appUserAddress, 430); + await appUserNFT.approve(swapKiwi.address, 430); + const tx = await appUser.proposeSwap(otherAppUserAddress, [appUserNFT.address], [430], [0], { + value: VALID_APP_FEE.add(parseEther("20")) + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + await otherAppUserNFT.mint(otherAppUserAddress, 431); + await otherAppUserNFT.approve(swapKiwi.address, 431); + const initiateSwapTx = await otherAppUser.initiateSwap( + swapIdFromLogs, + [otherAppUserNFT.address], + [431], + [0], + { + value: VALID_APP_FEE.add(parseEther("10")) + } + ); + const initiateSwapTxReceipt = await initiateSwapTx.wait(1); + await getEventWithArgsFromLogs(initiateSwapTxReceipt, "SwapInitiated"); + + const cancelTx = await otherAppUser.cancelSwap(swapIdFromLogs); + const cancelTxReceipt = await cancelTx.wait(1); + await getEventWithArgsFromLogs(cancelTxReceipt, "SwapCanceled"); + + expect(await appUserNFT.ownerOf(430)).to.be.deep.equal(appUserAddress); + expect(await appUserNFT.ownerOf(431)).to.be.deep.equal(otherAppUserAddress); + expect(firstUserBalance.sub(await appUser.signer.getBalance()).lt(parseEther("1"))).to.be.equal(true); + expect(secondUserBalance.sub(await otherAppUser.signer.getBalance()).lt(parseEther("1"))).to.be.equal(true); + }); + + it('Should fail to initiate swap if swap canceled', async function () { + await appUserNFT.mint(appUserAddress, 170); + await appUserNFT.approve(swapKiwi.address, 170); + const tx = await appUser.proposeSwap(otherAppUserAddress, [appUserNFT.address], [170], [0], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + const cancelTx = await appUser.cancelSwap(swapIdFromLogs); + await cancelTx.wait(1); + + await otherAppUserNFT.mint(otherAppUserAddress, 301); + await otherAppUserNFT.approve(swapKiwi.address, 301); + await expect(otherAppUser.initiateSwap(swapIdFromLogs, [otherAppUserNFT.address], [301], [0], { + value: VALID_APP_FEE + })).to.be.rejectedWith( + `SwapKiwi: caller is not swap participator` + ); + }); + + it('Should fail to initiate swap with invalid app fee', async function () { + const tx = await appUser.proposeSwap(otherAppUserAddress, [], [], [], { + value: VALID_APP_FEE.add(parseEther("0.1")) + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + await expect(otherAppUser.initiateSwap(swapIdFromLogs, [], [], [], { + value: parseEther("0.01") + })).to.be.rejectedWith( + `SwapKiwi: Sent ETH amount needs to be more or equal application fee` + ); + }); + + it('Should fail to initiate swap twice', async function () { + await appUserNFT.mint(appUserAddress, 189); + await appUserNFT.approve(swapKiwi.address, 189); + const tx = await appUser.proposeSwap(otherAppUserAddress, [appUserNFT.address], [189], [0], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + await otherAppUserNFT.mint(otherAppUserAddress, 302); + await otherAppUserNFT.approve(swapKiwi.address, 302); + await otherAppUser.initiateSwap(swapIdFromLogs, [otherAppUserNFT.address], [302], [0], { + value: VALID_APP_FEE + }) + + + await otherAppUserNFT.mint(otherAppUserAddress, 303); + await otherAppUserNFT.approve(swapKiwi.address, 303); + await expect(otherAppUser.initiateSwap(swapIdFromLogs, [otherAppUserNFT.address], [303], [0], { + value: VALID_APP_FEE + })).to.be.rejectedWith( + "SwapKiwi: swap already initiated" + ); + }); + + it('Should fail to initiate swap twice if proposed only with ether', async function () { + await appUserNFT.mint(appUserAddress, 1732); + await appUserNFT.approve(swapKiwi.address, 1732); + const tx = await appUser.proposeSwap(otherAppUserAddress, [appUserNFT.address], [1732], [0], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + await otherAppUser.initiateSwap(swapIdFromLogs, [], [], [], { + value: VALID_APP_FEE.add(ethers.utils.parseEther("0.25")) + }) + + + await otherAppUserNFT.mint(otherAppUserAddress, 1733); + await otherAppUserNFT.approve(swapKiwi.address, 1733); + await expect(otherAppUser.initiateSwap(swapIdFromLogs, [otherAppUserNFT.address], [1733], [0], { + value: VALID_APP_FEE + })).to.be.rejectedWith( + "SwapKiwi: swap already initiated" + ); + }); + + it('Should fail to cancel swap twice', async function () { + await appUserNFT.mint(appUserAddress, 200); + await appUserNFT.approve(swapKiwi.address, 200); + const tx = await appUser.proposeSwap(otherAppUserAddress, [appUserNFT.address], [200], [0], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + const cancelTx = await appUser.cancelSwap(swapIdFromLogs); + const cancelTxReceipt = await cancelTx.wait(1); + const cancelTxlogs = await getEventWithArgsFromLogs(cancelTxReceipt, "SwapCanceled"); + + // check if all values are emitted in event + expect(cancelTxlogs.eventName).to.be.deep.equal("SwapCanceled"); + expect(cancelTxlogs.args.canceledBy).to.be.deep.equal(appUserAddress); + // expect that swap ID from "SwapCanceled" is same as swap ID from "swapProposed" event + expect(cancelTxlogs.args.swapId.toString()).to.be.deep.equal(String(swapIdFromLogs)); + // check that NFT is returned to initial owner + expect(await appUserNFT.ownerOf(200)).to.be.deep.equal(appUserAddress); + + await expect(appUser.cancelSwap(swapIdFromLogs)).to.be.rejectedWith( + "SwapKiwi: Can't cancel swap, must be swap participant" + ); + }); + + it("Should fail to cancel swap if user is not a swap participant", async function () { + const nonSwapParticipant = new ethers.Contract(SwapKiwi.address, SwapKiwi.abi, signers[6]) as SwapKiwi; + + // first user NFT minting and swap deposit into SwapKiwi + await appUserNFT.mint(appUserAddress, 70); + await appUserNFT.approve(swapKiwi.address, 70); + const tx = await appUser.proposeSwap(otherAppUserAddress, [appUserNFT.address], [70], [0], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + await expect(nonSwapParticipant.cancelSwap(swapIdFromLogs)).to.be.rejectedWith( + "SwapKiwi: Can't cancel swap, must be swap participant"); + }); + + it("Should fail to accept swap if second user didn't add NFTs or ether", async function () { + // first user NFT minting and swap deposit into SwapKiwi + await appUserNFT.mint(appUserAddress, 2000); + await appUserNFT.approve(swapKiwi.address, 2000); + const tx = await appUser.proposeSwap(otherAppUserAddress, [appUserNFT.address], [2000], [0], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + await expect(appUser.acceptSwap(swapIdFromLogs)).to.be.rejectedWith( + "SwapKiwi: Can't accept swap, both participants didn't add NFTs"); + }); + + it('Should fail to accept swap if not swap initiator', async function () { + await appUserNFT.mint(appUserAddress, 2100); + await appUserNFT.approve(swapKiwi.address, 2100); + const tx = await appUser.proposeSwap(otherAppUserAddress, [appUserNFT.address], [2100], [0], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + const initiateSwapTx = await otherAppUser.initiateSwap( + swapIdFromLogs, + [], + [], + [], + { + value: VALID_APP_FEE.add(parseEther("50")) + } + ); + await initiateSwapTx.wait(1); + + await expect(otherAppUser.acceptSwap(swapIdFromLogs)).to.be.rejectedWith( + "SwapKiwi: caller is not swap initiator" + ); + }); + + it('Should successfully execute NFT - NFT swap', async function () { + // first user NFT minting and swap deposit into SwapKiwi + await appUserNFT.mint(appUserAddress, 85); + await appUserNFT.mint(appUserAddress, 86); + await appUserNFT.approve(swapKiwi.address, 85); + await appUserNFT.approve(swapKiwi.address, 86); + const tx = await appUser.proposeSwap(otherAppUserAddress, [appUserNFT.address, appUserNFT.address], [85, 86], [0, 0], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + // check that first user NFTs are deposited into SwapKiwi + expect(await appUserNFT.ownerOf(85)).to.be.deep.equal(swapKiwi.address); + expect(await appUserNFT.ownerOf(86)).to.be.deep.equal(swapKiwi.address); + + // second user NFT minting and swap deposit into SwapKiwi + await otherAppUserNFT.mint(otherAppUserAddress, 87); + await otherAppUserNFT.mint(otherAppUserAddress, 88); + await otherAppUserNFT.approve(swapKiwi.address, 87); + await otherAppUserNFT.approve(swapKiwi.address, 88); + const initiateSwapTx = await otherAppUser.initiateSwap( + swapIdFromLogs, + [otherAppUserNFT.address, otherAppUserNFT.address], + [87, 88], + [0, 0], + { + value: VALID_APP_FEE + } + ); + const initiateSwapTxReceipt = await initiateSwapTx.wait(1); + const initiateSwapLogs = await getEventWithArgsFromLogs(initiateSwapTxReceipt, "SwapInitiated"); + // check if all values are emitted in "SwapInitiated" event + expect(initiateSwapLogs.eventName).to.be.deep.equal("SwapInitiated"); + expect(initiateSwapLogs.args.from).to.be.deep.equal(otherAppUserAddress); + expect(initiateSwapLogs.args.to).to.be.deep.equal(appUserAddress); + + // check that second user NFTs are deposited into SwapKiwi + expect(await otherAppUserNFT.ownerOf(87)).to.be.deep.equal(swapKiwi.address); + expect(await otherAppUserNFT.ownerOf(88)).to.be.deep.equal(swapKiwi.address); + + const acceptSwapTx = await appUser.acceptSwap(swapIdFromLogs); + const acceptSwapTxReceipt = await acceptSwapTx.wait(1); + const acceptSwapLogs = await getEventWithArgsFromLogs(acceptSwapTxReceipt, "SwapExecuted"); + + // check if all values are emitted in "SwapExecuted" event + expect(acceptSwapLogs.eventName).to.be.deep.equal("SwapExecuted"); + expect(acceptSwapLogs.args.from).to.be.deep.equal(appUserAddress); + expect(acceptSwapLogs.args.to).to.be.deep.equal(otherAppUserAddress); + // check that NFTs are transfered from SwapKiwi to participants - same address because both have same signer + + expect(await otherAppUserNFT.ownerOf(85)).to.be.deep.equal(otherAppUserAddress); + expect(await otherAppUserNFT.ownerOf(86)).to.be.deep.equal(otherAppUserAddress); + expect(await appUserNFT.ownerOf(87)).to.be.deep.equal(appUserAddress); + expect(await appUserNFT.ownerOf(88)).to.be.deep.equal(appUserAddress); + }); + + it('Should successfully execute NFT + ether - NFT + ether swap', async function () { + const firstUserBalance = await appUser.signer.getBalance(); + const secondUserBalance = await otherAppUser.signer.getBalance(); + + await appUserNFT.mint(appUserAddress, 375); + await appUserNFT.approve(swapKiwi.address, 375); + const tx = await appUser.proposeSwap(otherAppUserAddress, [appUserNFT.address], [375], [0], { + value: VALID_APP_FEE.add(parseEther("50")) + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + await otherAppUserNFT.mint(otherAppUserAddress, 376); + await otherAppUserNFT.approve(swapKiwi.address, 376); + const initiateSwapTx = await otherAppUser.initiateSwap( + swapIdFromLogs, + [otherAppUserNFT.address], + [376], + [0], + { + value: VALID_APP_FEE.add(parseEther("25")) + } + ); + await initiateSwapTx.wait(1); + + const acceptSwapTx = await appUser.acceptSwap(swapIdFromLogs); + await acceptSwapTx.wait(1); + + expect(await appUserNFT.ownerOf(375)).to.be.deep.equal(otherAppUserAddress); + expect(await otherAppUserNFT.ownerOf(376)).to.be.deep.equal(appUserAddress); + expect(firstUserBalance.sub((await appUser.signer.getBalance()).add(parseEther("25"))).lt(parseEther("1"))).to.be.equal(true); + expect(secondUserBalance.sub((await otherAppUser.signer.getBalance()).sub(parseEther("25"))).lt(parseEther("1"))).to.be.equal(true); + }); + + it('Should successfully execute ether - NFT swap', async function () { + const secondUserBalance = await otherAppUser.signer.getBalance(); + + const tx = await appUser.proposeSwap(otherAppUserAddress, [], [], [], { + value: VALID_APP_FEE.add(parseEther("50")) + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + await otherAppUserNFT.mint(otherAppUserAddress, 1800); + await otherAppUserNFT.approve(swapKiwi.address, 1800); + const initiateSwapTx = await otherAppUser.initiateSwap( + swapIdFromLogs, + [otherAppUserNFT.address], + [1800], + [0], + { + value: VALID_APP_FEE + } + ); + await initiateSwapTx.wait(1); + + const acceptSwapTx = await appUser.acceptSwap(swapIdFromLogs); + await acceptSwapTx.wait(1); + + expect(await otherAppUserNFT.ownerOf(1800)).to.be.deep.equal(appUserAddress); + expect( + ( + await otherAppUser.signer.getBalance() + ).sub(secondUserBalance).sub(parseEther("50")).lt(parseEther("1"))).to.be.equal(true); + }); + + it('Should successfully execute NFT - ether swap', async function () { + const firstUserBalance = await appUser.signer.getBalance(); + + await appUserNFT.mint(appUserAddress, 1822); + await appUserNFT.approve(swapKiwi.address, 1822); + const tx = await appUser.proposeSwap(otherAppUserAddress, [appUserNFT.address], [1822], [0], { + value: VALID_APP_FEE + }); + const txReceipt = await tx.wait(1); + const logs = await getEventWithArgsFromLogs(txReceipt, "SwapProposed"); + const swapIdFromLogs = Number(logs.args.swapId.toString()); + + const initiateSwapTx = await otherAppUser.initiateSwap( + swapIdFromLogs, + [], + [], + [], + { + value: VALID_APP_FEE.add(parseEther("50")) + } + ); + await initiateSwapTx.wait(1); + + const acceptSwapTx = await appUser.acceptSwap(swapIdFromLogs); + await acceptSwapTx.wait(1); + + expect(await appUserNFT.ownerOf(1822)).to.be.deep.equal(otherAppUserAddress); + expect( + ( + await appUser.signer.getBalance() + ).sub(firstUserBalance).sub(parseEther("50")).lt(parseEther("1"))).to.be.equal(true); + }); + + it("Should successfully withdraw only collected fees", async function () { + await swapKiwi.withdrawEther(await signers[7].getAddress()); + + const tx1 = await appUser.proposeSwap(otherAppUserAddress, [], [], [], { + value: VALID_APP_FEE.add(ethers.utils.parseEther("1")) + }); + const txReceipt1 = await tx1.wait(1); + const logs1 = await getEventWithArgsFromLogs(txReceipt1, "SwapProposed"); + const swapIdFromLogs1 = Number(logs1.args.swapId.toString()); + const initiateSwapTx1 = await otherAppUser.initiateSwap( + swapIdFromLogs1, + [], + [], + [], + { + value: VALID_APP_FEE.add(parseEther("5")) + } + ); + await initiateSwapTx1.wait(1); + const acceptSwapTx1 = await appUser.acceptSwap(swapIdFromLogs1); + await acceptSwapTx1.wait(1); + const tx2 = await appUser.proposeSwap(otherAppUserAddress, [], [], [], { + value: VALID_APP_FEE.add(ethers.utils.parseEther("1")) + }); + const txReceipt2 = await tx2.wait(1); + const logs2 = await getEventWithArgsFromLogs(txReceipt2, "SwapProposed"); + const swapIdFromLogs = Number(logs2.args.swapId.toString()); + const initiateSwapTx2 = await otherAppUser.initiateSwap( + swapIdFromLogs, + [], + [], + [], + { + value: VALID_APP_FEE.add(parseEther("5")) + } + ); + await initiateSwapTx2.wait(1); + + await swapKiwi.withdrawEther(appUserNFT.address) + + expect((await ethers.provider.getBalance(appUserNFT.address)).toString()) + .to.be.deep.equal(VALID_APP_FEE.mul(4).toString()) + }); + + it("Should fail to withdraw collected fees if not owner", async function () { + await expect(appUser.withdrawEther(appUser.address)) + .to.be.rejectedWith( + "Ownable: caller is not the owner"); + }); + + it("Should fail to withdraw collected fees if sent to zero address", async function () { + await expect(swapKiwi.withdrawEther("0x0000000000000000000000000000000000000000")) + .to.be.rejectedWith("SwapKiwi: transfer to the zero address"); + }); +});