From 448ecb4ad912bd80f9707e2669afad693e3d65f3 Mon Sep 17 00:00:00 2001 From: akashiceth Date: Tue, 7 Jun 2022 15:55:40 +0800 Subject: [PATCH] feat: Add bend exchange --- .eslintignore | 1 + .github/CODEOWNERS | 2 +- .github/workflows/tests.yaml | 2 - .gitignore | 4 + .gitmodules | 3 - .prettierignore | 1 + .release-it.js | 7 +- .solcover.js | 2 +- .solhint.json | 5 +- .vscode/settings.json | 16 +- LICENSE | 2 +- README.md | 2 +- ...LooksRareExchange.sol => BendExchange.sol} | 376 ++- contracts/CurrencyManager.sol | 10 +- contracts/ExecutionManager.sol | 8 +- contracts/InterceptorManager.sol | 64 + contracts/RoyaltyFeeManager.sol | 2 +- contracts/TransferManager.sol | 89 + contracts/TransferSelectorNFT.sol | 90 - .../AuthenticatedProxy.sol | 74 + .../AuthorizationManager.sol | 43 + ...tegyAnyItemFromCollectionForFixedPrice.sol | 2 +- .../StrategyAnyItemInASetForFixedPrice.sol | 2 +- .../StrategyDutchAuction.sol | 10 +- .../StrategyPrivateSale.sol | 2 +- .../StrategyStandardSaleForFixedPrice.sol | 2 +- contracts/interceptors/RedeemNFT.sol | 69 + contracts/interceptors/interfaces/IBNFT.sol | 13 + .../interceptors/interfaces/ILendPool.sol | 119 + .../interfaces/ILendPoolAddressesProvider.sol | 15 + contracts/interfaces/IAuthenticatedProxy.sol | 18 + .../interfaces/IAuthorizationManager.sol | 16 + ...ooksRareExchange.sol => IBendExchange.sol} | 4 +- contracts/interfaces/ICurrencyManager.sol | 2 +- contracts/interfaces/IExecutionManager.sol | 2 +- contracts/interfaces/IExecutionStrategy.sol | 2 +- contracts/interfaces/IInterceptor.sol | 17 + contracts/interfaces/IInterceptorManager.sol | 17 + contracts/interfaces/IOwnable.sol | 2 +- contracts/interfaces/IRoyaltyFeeManager.sol | 2 +- contracts/interfaces/IRoyaltyFeeRegistry.sol | 2 +- contracts/interfaces/ITransfer.sol | 15 + contracts/interfaces/ITransferManager.sol | 12 + contracts/interfaces/ITransferManagerNFT.sol | 12 - contracts/interfaces/ITransferSelectorNFT.sol | 6 - contracts/interfaces/IWETH.sol | 2 + contracts/libraries/OrderTypes.sol | 60 +- contracts/libraries/SafeProxy.sol | 90 + contracts/libraries/SignatureChecker.sol | 8 +- .../royaltyFeeHelpers/RoyaltyFeeRegistry.sol | 10 +- .../royaltyFeeHelpers/RoyaltyFeeSetter.sol | 16 +- contracts/test/DutchAuction.t.sol | 81 - contracts/test/ICheatCodes.sol | 106 - contracts/test/MockBNFT.sol | 40 + contracts/test/{utils => }/MockERC1155.sol | 2 +- contracts/test/{utils => }/MockERC20.sol | 2 +- contracts/test/{utils => }/MockERC721.sol | 2 +- .../test/{utils => }/MockERC721WithAdmin.sol | 2 +- .../test/{utils => }/MockERC721WithOwner.sol | 2 +- .../{utils => }/MockERC721WithRoyalty.sol | 2 +- contracts/test/MockInterceptor.sol | 17 + contracts/test/MockLendPool.sol | 157 + .../test/MockLendPoolAddressesProvider.sol | 19 + .../{utils => }/MockNonCompliantERC721.sol | 2 +- contracts/test/MockNonPayable.sol | 13 + contracts/test/MockNonTransferERC20.sol | 22 + .../test/{utils => }/MockSignerContract.sol | 11 +- contracts/test/TestHelpers.sol | 51 - contracts/test/{utils => }/WETH.sol | 0 .../TransferManagerERC1155.sol | 42 - .../TransferManagerERC721.sol | 42 - .../TransferManagerNonCompliantERC721.sol | 39 - contracts/transfers/TransferERC1155.sol | 22 + contracts/transfers/TransferERC721.sol | 21 + .../transfers/TransferNonCompliantERC721.sol | 21 + foundry.toml | 36 - hardhat.config.ts | 10 +- lib/ds-test | 1 - package.json | 17 +- test/AuthorizationManager.test.ts | 163 + test/BendExchange.test.ts | 2919 +++++++++++++++++ test/CurrencyManager.test.ts | 51 + test/ExecutionManager.test.ts | 40 + test/InterceptorManager.test.ts | 41 + test/RedeemNFT.test.ts | 1125 +++++++ test/RoyaltyFeeSetter.test.ts | 259 ++ test/TransferManager.test.ts | 37 + test/_setup.ts | 339 ++ test/helpers/gas-helper.ts | 6 + test/helpers/hardhat-keys.ts | 3 + test/helpers/order-helper.ts | 12 +- test/helpers/order-types.ts | 6 +- test/helpers/signature-helper.ts | 22 +- test/looksRareExchange.test.ts | 2339 ------------- ...AnyItemFromCollectionForFixedPrice.test.ts | 184 ++ ...trategyAnyItemInASetForAFixedPrice.test.ts | 213 ++ test/strategies/strategyDutchAuction.test.ts | 299 ++ test/strategies/strategyPrivateSale.test.ts | 236 ++ ...AnyItemFromCollectionForFixedPrice.test.ts | 227 -- ...trategyAnyItemInASetForAFixedPrice.test.ts | 256 -- test/strategyDutchAuction.test.ts | 342 -- test/strategyPrivateSale.test.ts | 279 -- test/test-setup.ts | 148 - test/token-set-up.ts | 42 - 104 files changed, 7290 insertions(+), 4432 deletions(-) delete mode 100644 .gitmodules rename contracts/{LooksRareExchange.sol => BendExchange.sol} (59%) create mode 100644 contracts/InterceptorManager.sol create mode 100644 contracts/TransferManager.sol delete mode 100644 contracts/TransferSelectorNFT.sol create mode 100644 contracts/authorizationManager/AuthenticatedProxy.sol create mode 100644 contracts/authorizationManager/AuthorizationManager.sol create mode 100644 contracts/interceptors/RedeemNFT.sol create mode 100644 contracts/interceptors/interfaces/IBNFT.sol create mode 100644 contracts/interceptors/interfaces/ILendPool.sol create mode 100644 contracts/interceptors/interfaces/ILendPoolAddressesProvider.sol create mode 100644 contracts/interfaces/IAuthenticatedProxy.sol create mode 100644 contracts/interfaces/IAuthorizationManager.sol rename contracts/interfaces/{ILooksRareExchange.sol => IBendExchange.sol} (90%) create mode 100644 contracts/interfaces/IInterceptor.sol create mode 100644 contracts/interfaces/IInterceptorManager.sol create mode 100644 contracts/interfaces/ITransfer.sol create mode 100644 contracts/interfaces/ITransferManager.sol delete mode 100644 contracts/interfaces/ITransferManagerNFT.sol delete mode 100644 contracts/interfaces/ITransferSelectorNFT.sol create mode 100644 contracts/libraries/SafeProxy.sol delete mode 100644 contracts/test/DutchAuction.t.sol delete mode 100644 contracts/test/ICheatCodes.sol create mode 100644 contracts/test/MockBNFT.sol rename contracts/test/{utils => }/MockERC1155.sol (95%) rename contracts/test/{utils => }/MockERC20.sol (92%) rename contracts/test/{utils => }/MockERC721.sol (93%) rename contracts/test/{utils => }/MockERC721WithAdmin.sol (94%) rename contracts/test/{utils => }/MockERC721WithOwner.sol (95%) rename contracts/test/{utils => }/MockERC721WithRoyalty.sol (97%) create mode 100644 contracts/test/MockInterceptor.sol create mode 100644 contracts/test/MockLendPool.sol create mode 100644 contracts/test/MockLendPoolAddressesProvider.sol rename contracts/test/{utils => }/MockNonCompliantERC721.sol (96%) create mode 100644 contracts/test/MockNonPayable.sol create mode 100644 contracts/test/MockNonTransferERC20.sol rename contracts/test/{utils => }/MockSignerContract.sol (89%) delete mode 100644 contracts/test/TestHelpers.sol rename contracts/test/{utils => }/WETH.sol (100%) delete mode 100644 contracts/transferManagers/TransferManagerERC1155.sol delete mode 100644 contracts/transferManagers/TransferManagerERC721.sol delete mode 100644 contracts/transferManagers/TransferManagerNonCompliantERC721.sol create mode 100644 contracts/transfers/TransferERC1155.sol create mode 100644 contracts/transfers/TransferERC721.sol create mode 100644 contracts/transfers/TransferNonCompliantERC721.sol delete mode 100644 foundry.toml delete mode 160000 lib/ds-test create mode 100644 test/AuthorizationManager.test.ts create mode 100644 test/BendExchange.test.ts create mode 100644 test/CurrencyManager.test.ts create mode 100644 test/ExecutionManager.test.ts create mode 100644 test/InterceptorManager.test.ts create mode 100644 test/RedeemNFT.test.ts create mode 100644 test/RoyaltyFeeSetter.test.ts create mode 100644 test/TransferManager.test.ts create mode 100644 test/_setup.ts create mode 100644 test/helpers/gas-helper.ts delete mode 100644 test/looksRareExchange.test.ts create mode 100644 test/strategies/strategyAnyItemFromCollectionForFixedPrice.test.ts create mode 100644 test/strategies/strategyAnyItemInASetForAFixedPrice.test.ts create mode 100644 test/strategies/strategyDutchAuction.test.ts create mode 100644 test/strategies/strategyPrivateSale.test.ts delete mode 100644 test/strategyAnyItemFromCollectionForFixedPrice.test.ts delete mode 100644 test/strategyAnyItemInASetForAFixedPrice.test.ts delete mode 100644 test/strategyDutchAuction.test.ts delete mode 100644 test/strategyPrivateSale.test.ts delete mode 100644 test/test-setup.ts delete mode 100644 test/token-set-up.ts diff --git a/.eslintignore b/.eslintignore index 545a4ba..4fb6f5e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,3 +5,4 @@ coverage* gasReporterOutput.json typechain lib/ds-test +.history diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fea7b01..e35a339 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,3 @@ # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners -@0xShisui @0xJurassicPunk +@akashiceth diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 33dc932..9c057af 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -38,5 +38,3 @@ jobs: run: yarn compile:force - name: Run TypeScript/Waffle tests run: yarn test - - name: Run Solidity/Forge tests - run: forge test diff --git a/.gitignore b/.gitignore index f033c8b..17cab32 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,7 @@ abis/ # Others lib + +storage_layout +out.html +.history/* \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e124719..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "lib/ds-test"] - path = lib/ds-test - url = https://github.com/dapphub/ds-test diff --git a/.prettierignore b/.prettierignore index 545a4ba..4fb6f5e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,3 +5,4 @@ coverage* gasReporterOutput.json typechain lib/ds-test +.history diff --git a/.release-it.js b/.release-it.js index 6ffb241..ffd8fcc 100644 --- a/.release-it.js +++ b/.release-it.js @@ -6,7 +6,7 @@ module.exports = { commitMessage: "build: Release v${version}", requireUpstream: false, pushRepo: "upstream", // Push tags and commit to the remote `upstream` (fails if doesn't exist) - requireBranch: "master", // Push commit to the branch `master` (fail if on other branch) + requireBranch: "main", // Push commit to the branch `master` (fail if on other branch) requireCommits: true, // Require new commits since latest tag }, github: { @@ -15,4 +15,9 @@ module.exports = { hooks: { "after:bump": "yarn compile:force", }, + + npm: { + publish: false, + skipChecks: true, + }, }; diff --git a/.solcover.js b/.solcover.js index 755183e..91c0e2e 100644 --- a/.solcover.js +++ b/.solcover.js @@ -2,6 +2,6 @@ module.exports = { silent: true, measureStatementCoverage: true, measureFunctionCoverage: true, - skipFiles: ["interfaces", "test"], + skipFiles: ["interfaces", "test", "interceptors/interfaces"], configureYulOptimizer: true, }; diff --git a/.solhint.json b/.solhint.json index e398534..6d13a22 100644 --- a/.solhint.json +++ b/.solhint.json @@ -1,11 +1,12 @@ { "extends": "solhint:recommended", "rules": { - "compiler-version": ["error", "^0.8.0"], + "compiler-version": ["error", "0.8.9"], "func-visibility": [{ "ignoreConstructors": true }], "func-name-mixedcase": "off", "not-rely-on-time": "off", "reason-string": "off", - "var-name-mixedcase": "off" + "var-name-mixedcase": "off", + "avoid-low-level-calls": "off" } } diff --git a/.vscode/settings.json b/.vscode/settings.json index ad92582..7f8f3e7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,17 @@ { - "editor.formatOnSave": true + "editor.formatOnSave": false, + "files.exclude": { + "**/.git": true, // this is a default value + "**/.DS_Store": true, // this is a default value + ".history/**": true, // this is a default value + "**/node_modules": true, // this excludes all folders + // named "node_modules" from + // the explore tree + + // alternative version + "node_modules": true // this excludes the folder + // only from the root of + // your workspace + }, + "solidity.compileUsingRemoteVersion": "v0.8.9+commit.e5eed63a" } diff --git a/LICENSE b/LICENSE index c007d11..c9042e5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 LooksRare +Copyright (c) 2022 BendDAO Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index aa38e8c..ce154a8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Description -This project contains all smart contracts used for the current LooksRare exchange ("v1"). This includes: +This project contains all smart contracts used for the current BendDAO exchange ("v1"). This includes: - core exchange contract - libraries diff --git a/contracts/LooksRareExchange.sol b/contracts/BendExchange.sol similarity index 59% rename from contracts/LooksRareExchange.sol rename to contracts/BendExchange.sol index 4c37321..3075421 100644 --- a/contracts/LooksRareExchange.sol +++ b/contracts/BendExchange.sol @@ -1,77 +1,55 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; // OpenZeppelin contracts import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; -// LooksRare interfaces +// Bend interfaces +import {IAuthorizationManager} from "./interfaces/IAuthorizationManager.sol"; import {ICurrencyManager} from "./interfaces/ICurrencyManager.sol"; import {IExecutionManager} from "./interfaces/IExecutionManager.sol"; import {IExecutionStrategy} from "./interfaces/IExecutionStrategy.sol"; import {IRoyaltyFeeManager} from "./interfaces/IRoyaltyFeeManager.sol"; -import {ILooksRareExchange} from "./interfaces/ILooksRareExchange.sol"; -import {ITransferManagerNFT} from "./interfaces/ITransferManagerNFT.sol"; -import {ITransferSelectorNFT} from "./interfaces/ITransferSelectorNFT.sol"; +import {IBendExchange} from "./interfaces/IBendExchange.sol"; +import {ITransferManager} from "./interfaces/ITransferManager.sol"; +import {IInterceptorManager} from "./interfaces/IInterceptorManager.sol"; + import {IWETH} from "./interfaces/IWETH.sol"; +import {IAuthenticatedProxy} from "./interfaces/IAuthenticatedProxy.sol"; -// LooksRare libraries +// Bend libraries import {OrderTypes} from "./libraries/OrderTypes.sol"; import {SignatureChecker} from "./libraries/SignatureChecker.sol"; +import {SafeProxy} from "./libraries/SafeProxy.sol"; /** - * @title LooksRareExchange - * @notice It is the core contract of the LooksRare exchange. -LOOKSRARELOOKSRARELOOKSRLOOKSRARELOOKSRARELOOKSRARELOOKSRARELOOKSRLOOKSRARELOOKSRARELOOKSR -LOOKSRARELOOKSRARELOOKSRAR'''''''''''''''''''''''''''''''''''OOKSRLOOKSRARELOOKSRARELOOKSR -LOOKSRARELOOKSRARELOOKS:. .;OOKSRARELOOKSRARELOOKSR -LOOKSRARELOOKSRARELOO,. .,KSRARELOOKSRARELOOKSR -LOOKSRARELOOKSRAREL' ..',;:LOOKS::;,'.. 'RARELOOKSRARELOOKSR -LOOKSRARELOOKSRAR. .,:LOOKSRARELOOKSRARELO:,. .RELOOKSRARELOOKSR -LOOKSRARELOOKS:. .;RARELOOKSRARELOOKSRARELOOKSl;. .:OOKSRARELOOKSR -LOOKSRARELOO;. .'OKSRARELOOKSRARELOOKSRARELOOKSRARE'. .;KSRARELOOKSR -LOOKSRAREL,. .,LOOKSRARELOOK:;;:"""":;;;lELOOKSRARELO,. .,RARELOOKSR -LOOKSRAR. .;okLOOKSRAREx:. .;OOKSRARELOOK;. .RELOOKSR -LOOKS:. .:dOOOLOOKSRARE' .''''.. .OKSRARELOOKSR:. .LOOKSR -LOx;. .cKSRARELOOKSRAR' 'LOOKSRAR' .KSRARELOOKSRARc.. .OKSR -L;. .cxOKSRARELOOKSRAR. .LOOKS.RARE' ;kRARELOOKSRARExc. .;R -LO' .;oOKSRARELOOKSRAl. .LOOKS.RARE. :kRARELOOKSRAREo;. 'SR -LOOK;. .,KSRARELOOKSRAx, .;LOOKSR;. .oSRARELOOKSRAo,. .;OKSR -LOOKSk:. .'RARELOOKSRARd;. .... 'oOOOOOOOOOOxc'. .:LOOKSR -LOOKSRARc. .:dLOOKSRAREko;. .,lxOOOOOOOOOd:. .ARELOOKSR -LOOKSRARELo' .;oOKSRARELOOxoc;,....,;:ldkOOOOOOOOkd;. 'SRARELOOKSR -LOOKSRARELOOd,. .,lSRARELOOKSRARELOOKSRARELOOKSRkl,. .,OKSRARELOOKSR -LOOKSRARELOOKSx;. ..;oxELOOKSRARELOOKSRARELOkxl:.. .:LOOKSRARELOOKSR -LOOKSRARELOOKSRARc. .':cOKSRARELOOKSRALOc;'. .ARELOOKSRARELOOKSR -LOOKSRARELOOKSRARELl' ...'',,,,''... 'SRARELOOKSRARELOOKSR -LOOKSRARELOOKSRARELOOo,. .,OKSRARELOOKSRARELOOKSR -LOOKSRARELOOKSRARELOOKSx;. .;xOOKSRARELOOKSRARELOOKSR -LOOKSRARELOOKSRARELOOKSRLO:. .:SRLOOKSRARELOOKSRARELOOKSR -LOOKSRARELOOKSRARELOOKSRLOOKl. .lOKSRLOOKSRARELOOKSRARELOOKSR -LOOKSRARELOOKSRARELOOKSRLOOKSRo'. .'oLOOKSRLOOKSRARELOOKSRARELOOKSR -LOOKSRARELOOKSRARELOOKSRLOOKSRARd;. .;xRELOOKSRLOOKSRARELOOKSRARELOOKSR -LOOKSRARELOOKSRARELOOKSRLOOKSRARELO:. .:kRARELOOKSRLOOKSRARELOOKSRARELOOKSR -LOOKSRARELOOKSRARELOOKSRLOOKSRARELOOKl. .cOKSRARELOOKSRLOOKSRARELOOKSRARELOOKSR -LOOKSRARELOOKSRARELOOKSRLOOKSRARELOOKSRo' 'oLOOKSRARELOOKSRLOOKSRARELOOKSRARELOOKSR -LOOKSRARELOOKSRARELOOKSRLOOKSRARELOOKSRARE,. .,dRELOOKSRARELOOKSRLOOKSRARELOOKSRARELOOKSR -LOOKSRARELOOKSRARELOOKSRLOOKSRARELOOKSRARELOOKSRARELOOKSRARELOOKSRLOOKSRARELOOKSRARELOOKSR + * @title BendExchange + * @notice It is the core contract of the Bend exchange. */ -contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { +contract BendExchange is IBendExchange, ReentrancyGuard, Ownable { + using SafeProxy for IAuthenticatedProxy; using SafeERC20 for IERC20; using OrderTypes for OrderTypes.MakerOrder; using OrderTypes for OrderTypes.TakerOrder; + string public constant NAME = "BendExchange"; + string public constant VERSION = "1.0"; + address public immutable WETH; bytes32 public immutable DOMAIN_SEPARATOR; address public protocolFeeRecipient; + IAuthorizationManager public authorizationManager; ICurrencyManager public currencyManager; IExecutionManager public executionManager; IRoyaltyFeeManager public royaltyFeeManager; - ITransferSelectorNFT public transferSelectorNFT; + ITransferManager public transferManager; + IInterceptorManager public interceptorManager; mapping(address => uint256) public userMinOrderNonce; mapping(address => mapping(uint256 => bool)) private _isUserOrderNonceExecutedOrCancelled; @@ -82,7 +60,17 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { event NewExecutionManager(address indexed executionManager); event NewProtocolFeeRecipient(address indexed protocolFeeRecipient); event NewRoyaltyFeeManager(address indexed royaltyFeeManager); - event NewTransferSelectorNFT(address indexed transferSelectorNFT); + event NewTransferManager(address indexed transferManager); + event NewAuthorizationManager(address indexed authorizationManager); + event NewInterceptorManager(address indexed interceptorManager); + + event ProtocolFeePayment( + address indexed collection, + uint256 indexed tokenId, + address indexed protocolFeeRecipient, + address currency, + uint256 amount + ); event RoyaltyPayment( address indexed collection, @@ -93,7 +81,7 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { ); event TakerAsk( - bytes32 orderHash, // bid hash of the maker order + bytes32 makerOrderHash, // bid hash of the maker order uint256 orderNonce, // user order nonce address indexed taker, // sender address for the taker ask order address indexed maker, // maker address of the initial bid order @@ -106,7 +94,7 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { ); event TakerBid( - bytes32 orderHash, // ask hash of the maker order + bytes32 makerOrderHash, // ask hash of the maker order uint256 orderNonce, // user order nonce address indexed taker, // sender address for the taker bid order address indexed maker, // maker address of the initial ask order @@ -127,6 +115,8 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { * @param _protocolFeeRecipient protocol fee recipient */ constructor( + address _interceptorManager, + address _transferManager, address _currencyManager, address _executionManager, address _royaltyFeeManager, @@ -137,16 +127,18 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { DOMAIN_SEPARATOR = keccak256( abi.encode( 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f, // keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") - 0xda9101ba92939daf4bb2e18cd5f942363b9297fbc3232c9dd964abb1fb70ed71, // keccak256("LooksRareExchange") + 0xba0c660933e3f2279319fe2b72a6f829a2438d726bbe835523453fc0414c6020, // keccak256("BendExchange") 0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6, // keccak256(bytes("1")) for versionId = 1 block.chainid, address(this) ) ); + transferManager = ITransferManager(_transferManager); currencyManager = ICurrencyManager(_currencyManager); executionManager = IExecutionManager(_executionManager); royaltyFeeManager = IRoyaltyFeeManager(_royaltyFeeManager); + interceptorManager = IInterceptorManager(_interceptorManager); WETH = _WETH; protocolFeeRecipient = _protocolFeeRecipient; } @@ -156,8 +148,8 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { * @param minNonce minimum user nonce */ function cancelAllOrdersForSender(uint256 minNonce) external { - require(minNonce > userMinOrderNonce[msg.sender], "Cancel: Order nonce lower than current"); - require(minNonce < userMinOrderNonce[msg.sender] + 500000, "Cancel: Cannot cancel more orders"); + require(minNonce > userMinOrderNonce[msg.sender], "Cancel: order nonce lower than current"); + require(minNonce < userMinOrderNonce[msg.sender] + 500000, "Cancel: can not cancel more orders"); userMinOrderNonce[msg.sender] = minNonce; emit CancelAllOrders(msg.sender, minNonce); @@ -168,10 +160,10 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { * @param orderNonces array of order nonces */ function cancelMultipleMakerOrders(uint256[] calldata orderNonces) external { - require(orderNonces.length > 0, "Cancel: Cannot be empty"); + require(orderNonces.length > 0, "Cancel: can not be empty"); for (uint256 i = 0; i < orderNonces.length; i++) { - require(orderNonces[i] >= userMinOrderNonce[msg.sender], "Cancel: Order nonce lower than current"); + require(orderNonces[i] >= userMinOrderNonce[msg.sender], "Cancel: order nonce lower than current"); _isUserOrderNonceExecutedOrCancelled[msg.sender][orderNonces[i]] = true; } @@ -187,51 +179,60 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { OrderTypes.TakerOrder calldata takerBid, OrderTypes.MakerOrder calldata makerAsk ) external payable override nonReentrant { - require((makerAsk.isOrderAsk) && (!takerBid.isOrderAsk), "Order: Wrong sides"); - require(makerAsk.currency == WETH, "Order: Currency must be WETH"); - require(msg.sender == takerBid.taker, "Order: Taker must be the sender"); + require((makerAsk.isOrderAsk) && (!takerBid.isOrderAsk), "Order: wrong sides"); + require(makerAsk.currency == WETH || makerAsk.currency == address(0), "Order: currency must be WETH or ETH"); + require(msg.sender == takerBid.taker, "Order: taker must be the sender"); // If not enough ETH to cover the price, use WETH - if (takerBid.price > msg.value) { - IERC20(WETH).safeTransferFrom(msg.sender, address(this), (takerBid.price - msg.value)); - } else { - require(takerBid.price == msg.value, "Order: Msg.value too high"); - } + require(takerBid.price >= msg.value, "Order: msg.value too high"); // Wrap ETH sent to this contract IWETH(WETH).deposit{value: msg.value}(); + // Sent WETH back to sender + IERC20(WETH).safeTransferFrom(address(this), msg.sender, msg.value); + // Check the maker ask order bytes32 askHash = makerAsk.hash(); - _validateOrder(makerAsk, askHash); + _validateOrders(makerAsk, askHash, takerBid); // Retrieve execution parameters (bool isExecutionValid, uint256 tokenId, uint256 amount) = IExecutionStrategy(makerAsk.strategy) .canExecuteTakerBid(takerBid, makerAsk); - require(isExecutionValid, "Strategy: Execution invalid"); + require(isExecutionValid, "Strategy: execution invalid"); // Update maker ask order status to true (prevents replay) - _isUserOrderNonceExecutedOrCancelled[makerAsk.signer][makerAsk.nonce] = true; + _isUserOrderNonceExecutedOrCancelled[makerAsk.maker][makerAsk.nonce] = true; - // Execution part 1/2 - _transferFeesAndFundsWithWETH( + _transferFeesAndFunds( makerAsk.strategy, makerAsk.collection, tokenId, - makerAsk.signer, + makerAsk.currency, + msg.sender, + makerAsk.maker, takerBid.price, makerAsk.minPercentageToAsk ); - // Execution part 2/2 - _transferNonFungibleToken(makerAsk.collection, makerAsk.signer, takerBid.taker, tokenId, amount); + _transferNonFungibleToken( + makerAsk.interceptor, + makerAsk.interceptorExtra, + makerAsk.collection, + makerAsk.maker, + takerBid.taker, + tokenId, + amount + ); + + _withdrawFunds(makerAsk.currency, makerAsk.maker); emit TakerBid( askHash, makerAsk.nonce, takerBid.taker, - makerAsk.signer, + makerAsk.maker, makerAsk.strategy, makerAsk.currency, makerAsk.collection, @@ -251,41 +252,49 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { override nonReentrant { - require((makerAsk.isOrderAsk) && (!takerBid.isOrderAsk), "Order: Wrong sides"); - require(msg.sender == takerBid.taker, "Order: Taker must be the sender"); + require((makerAsk.isOrderAsk) && (!takerBid.isOrderAsk), "Order: wrong sides"); + require(msg.sender == takerBid.taker, "Order: taker must be the sender"); // Check the maker ask order bytes32 askHash = makerAsk.hash(); - _validateOrder(makerAsk, askHash); + _validateOrders(makerAsk, askHash, takerBid); (bool isExecutionValid, uint256 tokenId, uint256 amount) = IExecutionStrategy(makerAsk.strategy) .canExecuteTakerBid(takerBid, makerAsk); - require(isExecutionValid, "Strategy: Execution invalid"); + require(isExecutionValid, "Strategy: execution invalid"); // Update maker ask order status to true (prevents replay) - _isUserOrderNonceExecutedOrCancelled[makerAsk.signer][makerAsk.nonce] = true; + _isUserOrderNonceExecutedOrCancelled[makerAsk.maker][makerAsk.nonce] = true; - // Execution part 1/2 _transferFeesAndFunds( makerAsk.strategy, makerAsk.collection, tokenId, makerAsk.currency, msg.sender, - makerAsk.signer, + makerAsk.maker, takerBid.price, makerAsk.minPercentageToAsk ); - // Execution part 2/2 - _transferNonFungibleToken(makerAsk.collection, makerAsk.signer, takerBid.taker, tokenId, amount); + _transferNonFungibleToken( + makerAsk.interceptor, + makerAsk.interceptorExtra, + makerAsk.collection, + makerAsk.maker, + takerBid.taker, + tokenId, + amount + ); + + _withdrawFunds(makerAsk.currency, makerAsk.maker); emit TakerBid( askHash, makerAsk.nonce, takerBid.taker, - makerAsk.signer, + makerAsk.maker, makerAsk.strategy, makerAsk.currency, makerAsk.collection, @@ -305,41 +314,48 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { override nonReentrant { - require((!makerBid.isOrderAsk) && (takerAsk.isOrderAsk), "Order: Wrong sides"); - require(msg.sender == takerAsk.taker, "Order: Taker must be the sender"); + require((!makerBid.isOrderAsk) && (takerAsk.isOrderAsk), "Order: wrong sides"); + require(msg.sender == takerAsk.taker, "Order: taker must be the sender"); // Check the maker bid order bytes32 bidHash = makerBid.hash(); - _validateOrder(makerBid, bidHash); + _validateOrders(makerBid, bidHash, takerAsk); (bool isExecutionValid, uint256 tokenId, uint256 amount) = IExecutionStrategy(makerBid.strategy) .canExecuteTakerAsk(takerAsk, makerBid); - - require(isExecutionValid, "Strategy: Execution invalid"); + require(isExecutionValid, "Strategy: execution invalid"); // Update maker bid order status to true (prevents replay) - _isUserOrderNonceExecutedOrCancelled[makerBid.signer][makerBid.nonce] = true; + _isUserOrderNonceExecutedOrCancelled[makerBid.maker][makerBid.nonce] = true; - // Execution part 1/2 - _transferNonFungibleToken(makerBid.collection, msg.sender, makerBid.signer, tokenId, amount); - - // Execution part 2/2 _transferFeesAndFunds( makerBid.strategy, makerBid.collection, tokenId, makerBid.currency, - makerBid.signer, + makerBid.maker, takerAsk.taker, takerAsk.price, takerAsk.minPercentageToAsk ); + _transferNonFungibleToken( + takerAsk.interceptor, + takerAsk.interceptorExtra, + makerBid.collection, + msg.sender, + makerBid.maker, + tokenId, + amount + ); + + _withdrawFunds(makerBid.currency, takerAsk.taker); + emit TakerAsk( bidHash, makerBid.nonce, takerAsk.taker, - makerBid.signer, + makerBid.maker, makerBid.strategy, makerBid.currency, makerBid.collection, @@ -354,7 +370,7 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { * @param _currencyManager new currency manager address */ function updateCurrencyManager(address _currencyManager) external onlyOwner { - require(_currencyManager != address(0), "Owner: Cannot be null address"); + require(_currencyManager != address(0), "Owner: can not be null address"); currencyManager = ICurrencyManager(_currencyManager); emit NewCurrencyManager(_currencyManager); } @@ -364,7 +380,7 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { * @param _executionManager new execution manager address */ function updateExecutionManager(address _executionManager) external onlyOwner { - require(_executionManager != address(0), "Owner: Cannot be null address"); + require(_executionManager != address(0), "Owner: can not be null address"); executionManager = IExecutionManager(_executionManager); emit NewExecutionManager(_executionManager); } @@ -383,20 +399,27 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { * @param _royaltyFeeManager new fee manager address */ function updateRoyaltyFeeManager(address _royaltyFeeManager) external onlyOwner { - require(_royaltyFeeManager != address(0), "Owner: Cannot be null address"); + require(_royaltyFeeManager != address(0), "Owner: can not be null address"); royaltyFeeManager = IRoyaltyFeeManager(_royaltyFeeManager); emit NewRoyaltyFeeManager(_royaltyFeeManager); } - /** - * @notice Update transfer selector NFT - * @param _transferSelectorNFT new transfer selector address - */ - function updateTransferSelectorNFT(address _transferSelectorNFT) external onlyOwner { - require(_transferSelectorNFT != address(0), "Owner: Cannot be null address"); - transferSelectorNFT = ITransferSelectorNFT(_transferSelectorNFT); + function updateTransferManager(address _transferManager) external onlyOwner { + require(_transferManager != address(0), "Owner: can not be null address"); + transferManager = ITransferManager(_transferManager); + emit NewTransferManager(_transferManager); + } - emit NewTransferSelectorNFT(_transferSelectorNFT); + function updateAuthorizationManager(address _authorizationManager) external onlyOwner { + require(_authorizationManager != address(0), "Owner: can not be null address"); + authorizationManager = IAuthorizationManager(_authorizationManager); + emit NewAuthorizationManager(_authorizationManager); + } + + function updateInterceptorManager(address _interceptorManager) external onlyOwner { + require(_interceptorManager != address(0), "Owner: can not be null address"); + interceptorManager = IInterceptorManager(_interceptorManager); + emit NewInterceptorManager(_interceptorManager); } /** @@ -408,6 +431,19 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { return _isUserOrderNonceExecutedOrCancelled[user][orderNonce]; } + function _withdrawFunds(address currency, address recipient) internal { + IAuthenticatedProxy proxy = IAuthenticatedProxy(authorizationManager.proxies(recipient)); + if (_isNativeETH(currency)) { + proxy.withdrawETH(); + } else { + proxy.withdrawToken(currency); + } + } + + function _isNativeETH(address currency) internal pure returns (bool) { + return currency == address(0); + } + /** * @notice Transfer fees and funds to royalty recipient, protocol, and seller * @param strategy address of the execution strategy @@ -429,61 +465,17 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { uint256 amount, uint256 minPercentageToAsk ) internal { + IAuthenticatedProxy fromProxy = IAuthenticatedProxy(authorizationManager.proxies(from)); + IAuthenticatedProxy toProxy = IAuthenticatedProxy(authorizationManager.proxies(to)); + require(address(fromProxy) != address(0), "Authorization: no delegate proxy"); + require(address(toProxy) != address(0), "Authorization: no delegate proxy"); + // Initialize the final amount that is transferred to seller uint256 finalSellerAmount = amount; - // 1. Protocol fee - { - uint256 protocolFeeAmount = _calculateProtocolFee(strategy, amount); - - // Check if the protocol fee is different than 0 for this strategy - if ((protocolFeeRecipient != address(0)) && (protocolFeeAmount != 0)) { - IERC20(currency).safeTransferFrom(from, protocolFeeRecipient, protocolFeeAmount); - finalSellerAmount -= protocolFeeAmount; - } - } - - // 2. Royalty fee - { - (address royaltyFeeRecipient, uint256 royaltyFeeAmount) = royaltyFeeManager - .calculateRoyaltyFeeAndGetRecipient(collection, tokenId, amount); - - // Check if there is a royalty fee and that it is different to 0 - if ((royaltyFeeRecipient != address(0)) && (royaltyFeeAmount != 0)) { - IERC20(currency).safeTransferFrom(from, royaltyFeeRecipient, royaltyFeeAmount); - finalSellerAmount -= royaltyFeeAmount; - - emit RoyaltyPayment(collection, tokenId, royaltyFeeRecipient, currency, royaltyFeeAmount); - } - } - - require((finalSellerAmount * 10000) >= (minPercentageToAsk * amount), "Fees: Higher than expected"); - - // 3. Transfer final amount (post-fees) to seller - { - IERC20(currency).safeTransferFrom(from, to, finalSellerAmount); + if (_isNativeETH(currency)) { + currency = WETH; } - } - - /** - * @notice Transfer fees and funds to royalty recipient, protocol, and seller - * @param strategy address of the execution strategy - * @param collection non fungible token address for the transfer - * @param tokenId tokenId - * @param to seller's recipient - * @param amount amount being transferred (in currency) - * @param minPercentageToAsk minimum percentage of the gross amount that goes to ask - */ - function _transferFeesAndFundsWithWETH( - address strategy, - address collection, - uint256 tokenId, - address to, - uint256 amount, - uint256 minPercentageToAsk - ) internal { - // Initialize the final amount that is transferred to seller - uint256 finalSellerAmount = amount; // 1. Protocol fee { @@ -491,8 +483,9 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { // Check if the protocol fee is different than 0 for this strategy if ((protocolFeeRecipient != address(0)) && (protocolFeeAmount != 0)) { - IERC20(WETH).safeTransfer(protocolFeeRecipient, protocolFeeAmount); + fromProxy.safeTransfer(currency, protocolFeeRecipient, protocolFeeAmount); finalSellerAmount -= protocolFeeAmount; + emit ProtocolFeePayment(collection, tokenId, protocolFeeRecipient, currency, protocolFeeAmount); } } @@ -503,18 +496,17 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { // Check if there is a royalty fee and that it is different to 0 if ((royaltyFeeRecipient != address(0)) && (royaltyFeeAmount != 0)) { - IERC20(WETH).safeTransfer(royaltyFeeRecipient, royaltyFeeAmount); + fromProxy.safeTransfer(currency, royaltyFeeRecipient, royaltyFeeAmount); finalSellerAmount -= royaltyFeeAmount; - - emit RoyaltyPayment(collection, tokenId, royaltyFeeRecipient, address(WETH), royaltyFeeAmount); + emit RoyaltyPayment(collection, tokenId, royaltyFeeRecipient, currency, royaltyFeeAmount); } } - require((finalSellerAmount * 10000) >= (minPercentageToAsk * amount), "Fees: Higher than expected"); + require((finalSellerAmount * 10000) >= (minPercentageToAsk * amount), "Fees: higher than expected"); // 3. Transfer final amount (post-fees) to seller { - IERC20(WETH).safeTransfer(to, finalSellerAmount); + fromProxy.safeTransfer(currency, address(toProxy), finalSellerAmount); } } @@ -528,20 +520,33 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { * @dev For ERC721, amount is not used */ function _transferNonFungibleToken( + address interceptor, + bytes memory InterceptorExtra, address collection, address from, address to, uint256 tokenId, uint256 amount ) internal { + IAuthenticatedProxy proxy = IAuthenticatedProxy(authorizationManager.proxies(from)); + require(address(proxy) != address(0), "Authorization: no delegate proxy"); + // Retrieve the transfer manager address - address transferManager = transferSelectorNFT.checkTransferManagerForToken(collection); + address transfer = transferManager.checkTransferForToken(collection); - // If no transfer manager found, it returns address(0) - require(transferManager != address(0), "Transfer: No NFT transfer manager available"); + // If no transfer found, it returns address(0) + require(transfer != address(0), "Transfer: no NFT transfer available"); - // If one is found, transfer the token - ITransferManagerNFT(transferManager).transferNonFungibleToken(collection, from, to, tokenId, amount); + proxy.safeTransferNonFungibleTokenFrom( + transfer, + interceptor, + collection, + from, + to, + tokenId, + amount, + InterceptorExtra + ); } /** @@ -557,27 +562,31 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { /** * @notice Verify the validity of the maker order * @param makerOrder maker order - * @param orderHash computed hash for the order + * @param makerOrderHash computed hash for the order */ - function _validateOrder(OrderTypes.MakerOrder calldata makerOrder, bytes32 orderHash) internal view { + function _validateOrders( + OrderTypes.MakerOrder calldata makerOrder, + bytes32 makerOrderHash, + OrderTypes.TakerOrder calldata takerOrder + ) internal view { // Verify whether order nonce has expired require( - (!_isUserOrderNonceExecutedOrCancelled[makerOrder.signer][makerOrder.nonce]) && - (makerOrder.nonce >= userMinOrderNonce[makerOrder.signer]), - "Order: Matching order expired" + (!_isUserOrderNonceExecutedOrCancelled[makerOrder.maker][makerOrder.nonce]) && + (makerOrder.nonce >= userMinOrderNonce[makerOrder.maker]), + "Order: matching order expired" ); - // Verify the signer is not address(0) - require(makerOrder.signer != address(0), "Order: Invalid signer"); + // Verify the maker is not address(0) + require(makerOrder.maker != address(0), "Order: invalid maker"); // Verify the amount is not 0 - require(makerOrder.amount > 0, "Order: Amount cannot be 0"); + require(makerOrder.amount > 0, "Order: amount cannot be 0"); // Verify the validity of the signature require( SignatureChecker.verify( - orderHash, - makerOrder.signer, + makerOrderHash, + makerOrder.maker, makerOrder.v, makerOrder.r, makerOrder.s, @@ -586,10 +595,25 @@ contract LooksRareExchange is ILooksRareExchange, ReentrancyGuard, Ownable { "Signature: Invalid" ); - // Verify whether the currency is whitelisted - require(currencyManager.isCurrencyWhitelisted(makerOrder.currency), "Currency: Not whitelisted"); + // Verify whether the currency is whitelisted, address(0) means native ETH + if (makerOrder.currency != address(0)) { + require(currencyManager.isCurrencyWhitelisted(makerOrder.currency), "Currency: not whitelisted"); + } // Verify whether strategy can be executed - require(executionManager.isStrategyWhitelisted(makerOrder.strategy), "Strategy: Not whitelisted"); + require(executionManager.isStrategyWhitelisted(makerOrder.strategy), "Strategy: not whitelisted"); + + if (makerOrder.interceptor != address(0)) { + require( + makerOrder.isOrderAsk && interceptorManager.isInterceptorWhitelisted(makerOrder.interceptor), + "Interceptor: maker interceptor not whitelisted" + ); + } + if (takerOrder.interceptor != address(0)) { + require( + takerOrder.isOrderAsk && interceptorManager.isInterceptorWhitelisted(takerOrder.interceptor), + "Interceptor: taker interceptor not whitelisted" + ); + } } } diff --git a/contracts/CurrencyManager.sol b/contracts/CurrencyManager.sol index 0970ba8..5dc4d81 100644 --- a/contracts/CurrencyManager.sol +++ b/contracts/CurrencyManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; @@ -8,7 +8,7 @@ import {ICurrencyManager} from "./interfaces/ICurrencyManager.sol"; /** * @title CurrencyManager - * @notice It allows adding/removing currencies for trading on the LooksRare exchange. + * @notice It allows adding/removing currencies for trading on the Bend exchange. */ contract CurrencyManager is ICurrencyManager, Ownable { using EnumerableSet for EnumerableSet.AddressSet; @@ -23,9 +23,9 @@ contract CurrencyManager is ICurrencyManager, Ownable { * @param currency address of the currency to add */ function addCurrency(address currency) external override onlyOwner { - require(!_whitelistedCurrencies.contains(currency), "Currency: Already whitelisted"); + require(currency != address(0), "Currency: can not be null address"); + require(!_whitelistedCurrencies.contains(currency), "Currency: already whitelisted"); _whitelistedCurrencies.add(currency); - emit CurrencyWhitelisted(currency); } @@ -34,7 +34,7 @@ contract CurrencyManager is ICurrencyManager, Ownable { * @param currency address of the currency to remove */ function removeCurrency(address currency) external override onlyOwner { - require(_whitelistedCurrencies.contains(currency), "Currency: Not whitelisted"); + require(_whitelistedCurrencies.contains(currency), "Currency: not whitelisted"); _whitelistedCurrencies.remove(currency); emit CurrencyRemoved(currency); diff --git a/contracts/ExecutionManager.sol b/contracts/ExecutionManager.sol index ce5cdcd..044bb3d 100644 --- a/contracts/ExecutionManager.sol +++ b/contracts/ExecutionManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; @@ -8,7 +8,7 @@ import {IExecutionManager} from "./interfaces/IExecutionManager.sol"; /** * @title ExecutionManager - * @notice It allows adding/removing execution strategies for trading on the LooksRare exchange. + * @notice It allows adding/removing execution strategies for trading on the Bend exchange. */ contract ExecutionManager is IExecutionManager, Ownable { using EnumerableSet for EnumerableSet.AddressSet; @@ -23,7 +23,7 @@ contract ExecutionManager is IExecutionManager, Ownable { * @param strategy address of the strategy to add */ function addStrategy(address strategy) external override onlyOwner { - require(!_whitelistedStrategies.contains(strategy), "Strategy: Already whitelisted"); + require(!_whitelistedStrategies.contains(strategy), "Strategy: already whitelisted"); _whitelistedStrategies.add(strategy); emit StrategyWhitelisted(strategy); @@ -34,7 +34,7 @@ contract ExecutionManager is IExecutionManager, Ownable { * @param strategy address of the strategy to remove */ function removeStrategy(address strategy) external override onlyOwner { - require(_whitelistedStrategies.contains(strategy), "Strategy: Not whitelisted"); + require(_whitelistedStrategies.contains(strategy), "Strategy: not whitelisted"); _whitelistedStrategies.remove(strategy); emit StrategyRemoved(strategy); diff --git a/contracts/InterceptorManager.sol b/contracts/InterceptorManager.sol new file mode 100644 index 0000000..7880b26 --- /dev/null +++ b/contracts/InterceptorManager.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import {IInterceptor} from "./interfaces/IInterceptor.sol"; +import {IInterceptorManager} from "./interfaces/IInterceptorManager.sol"; + +/** + * @title InterceptorManager + * @notice It allows adding/removing inteceptor for trading on the Bend exchange. + */ +contract InterceptorManager is IInterceptorManager, Ownable { + using EnumerableSet for EnumerableSet.AddressSet; + EnumerableSet.AddressSet private _whitelistedInterceptors; + + event CollectionInterceptorRemoved(address indexed Interceptor); + event CollectionInterceptorWhitelisted(address indexed Interceptor); + + function addCollectionInterceptor(address Interceptor) external override onlyOwner { + require(Interceptor != address(0), "Interceptor: can not be null address"); + require(!_whitelistedInterceptors.contains(Interceptor), "Interceptor: already whitelisted"); + _whitelistedInterceptors.add(Interceptor); + emit CollectionInterceptorWhitelisted(Interceptor); + } + + function removeCollectionInterceptor(address Interceptor) external override onlyOwner { + require(_whitelistedInterceptors.contains(Interceptor), "Interceptor: not whitelisted"); + _whitelistedInterceptors.remove(Interceptor); + + emit CollectionInterceptorRemoved(Interceptor); + } + + function isInterceptorWhitelisted(address Interceptor) external view override returns (bool) { + return _whitelistedInterceptors.contains(Interceptor); + } + + function viewCountWhitelistedInterceptors() external view override returns (uint256) { + return _whitelistedInterceptors.length(); + } + + function viewWhitelistedInterceptors(uint256 cursor, uint256 size) + external + view + override + returns (address[] memory, uint256) + { + uint256 length = size; + + if (length > _whitelistedInterceptors.length() - cursor) { + length = _whitelistedInterceptors.length() - cursor; + } + + address[] memory whitelistedInterceptors = new address[](length); + + for (uint256 i = 0; i < length; i++) { + whitelistedInterceptors[i] = _whitelistedInterceptors.at(cursor + i); + } + + return (whitelistedInterceptors, cursor + length); + } +} diff --git a/contracts/RoyaltyFeeManager.sol b/contracts/RoyaltyFeeManager.sol index 506b74d..aa7f0ec 100644 --- a/contracts/RoyaltyFeeManager.sol +++ b/contracts/RoyaltyFeeManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IERC165, IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol"; diff --git a/contracts/TransferManager.sol b/contracts/TransferManager.sol new file mode 100644 index 0000000..64a49b9 --- /dev/null +++ b/contracts/TransferManager.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import {ITransfer} from "./interfaces/ITransfer.sol"; +import {ITransferManager} from "./interfaces/ITransferManager.sol"; + +/** + * @title TransferManager + * @notice It selects the NFT transfer based on a collection address. + */ +contract TransferManager is ITransferManager, Ownable { + // ERC721 interfaceID + bytes4 public constant INTERFACE_ID_ERC721 = 0x80ac58cd; + // ERC1155 interfaceID + bytes4 public constant INTERFACE_ID_ERC1155 = 0xd9b67a26; + + // Address of the transfer contract for ERC721 tokens + address public immutable TRANSFER_ERC721; + + // Address of the transfer contract for ERC1155 tokens + address public immutable TRANSFER_ERC1155; + + // Map collection address to transfer address + mapping(address => address) public transfers; + + event CollectionTransferAdded(address indexed collection, address indexed transfer); + event CollectionTransferRemoved(address indexed collection); + + event CollectionInterceptorRemoved(address indexed interceptor); + event CollectionInterceptorWhitelisted(address indexed interceptor); + + /** + * @notice Constructor + * @param _transferERC721 address of the ERC721 transfer + * @param _transferERC1155 address of the ERC1155 transfer + */ + constructor(address _transferERC721, address _transferERC1155) { + TRANSFER_ERC721 = _transferERC721; + TRANSFER_ERC1155 = _transferERC1155; + } + + /** + * @notice Add a transfer for a collection + * @param collection collection address to add specific transfer rule + * @dev It is meant to be used for exceptions only (e.g., CryptoKitties) + */ + function addCollectionTransfer(address collection, address transfer) external onlyOwner { + require(collection != address(0), "Owner: collection cannot be null address"); + require(transfer != address(0), "Owner: transfer cannot be null address"); + transfers[collection] = transfer; + + emit CollectionTransferAdded(collection, transfer); + } + + /** + * @notice Remove a transfer for a collection + * @param collection collection address to remove exception + */ + function removeCollectionTransfer(address collection) external onlyOwner { + require(transfers[collection] != address(0), "Owner: collection has no transfer"); + + // Set it to the address(0) + transfers[collection] = address(0); + + emit CollectionTransferRemoved(collection); + } + + /** + * @notice Check the transfer for a token + * @param collection collection address + * @dev Support for ERC165 interface is checked AFTER custom implementation + */ + function checkTransferForToken(address collection) external view override returns (address transfer) { + // Assign transfer (if any) + transfer = transfers[collection]; + + if (transfer == address(0)) { + if (IERC165(collection).supportsInterface(INTERFACE_ID_ERC721)) { + transfer = TRANSFER_ERC721; + } else if (IERC165(collection).supportsInterface(INTERFACE_ID_ERC1155)) { + transfer = TRANSFER_ERC1155; + } + } + } +} diff --git a/contracts/TransferSelectorNFT.sol b/contracts/TransferSelectorNFT.sol deleted file mode 100644 index a1b6c4b..0000000 --- a/contracts/TransferSelectorNFT.sol +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; - -import {ITransferSelectorNFT} from "./interfaces/ITransferSelectorNFT.sol"; - -/** - * @title TransferSelectorNFT - * @notice It selects the NFT transfer manager based on a collection address. - */ -contract TransferSelectorNFT is ITransferSelectorNFT, Ownable { - // ERC721 interfaceID - bytes4 public constant INTERFACE_ID_ERC721 = 0x80ac58cd; - // ERC1155 interfaceID - bytes4 public constant INTERFACE_ID_ERC1155 = 0xd9b67a26; - - // Address of the transfer manager contract for ERC721 tokens - address public immutable TRANSFER_MANAGER_ERC721; - - // Address of the transfer manager contract for ERC1155 tokens - address public immutable TRANSFER_MANAGER_ERC1155; - - // Map collection address to transfer manager address - mapping(address => address) public transferManagerSelectorForCollection; - - event CollectionTransferManagerAdded(address indexed collection, address indexed transferManager); - event CollectionTransferManagerRemoved(address indexed collection); - - /** - * @notice Constructor - * @param _transferManagerERC721 address of the ERC721 transfer manager - * @param _transferManagerERC1155 address of the ERC1155 transfer manager - */ - constructor(address _transferManagerERC721, address _transferManagerERC1155) { - TRANSFER_MANAGER_ERC721 = _transferManagerERC721; - TRANSFER_MANAGER_ERC1155 = _transferManagerERC1155; - } - - /** - * @notice Add a transfer manager for a collection - * @param collection collection address to add specific transfer rule - * @dev It is meant to be used for exceptions only (e.g., CryptoKitties) - */ - function addCollectionTransferManager(address collection, address transferManager) external onlyOwner { - require(collection != address(0), "Owner: Collection cannot be null address"); - require(transferManager != address(0), "Owner: TransferManager cannot be null address"); - - transferManagerSelectorForCollection[collection] = transferManager; - - emit CollectionTransferManagerAdded(collection, transferManager); - } - - /** - * @notice Remove a transfer manager for a collection - * @param collection collection address to remove exception - */ - function removeCollectionTransferManager(address collection) external onlyOwner { - require( - transferManagerSelectorForCollection[collection] != address(0), - "Owner: Collection has no transfer manager" - ); - - // Set it to the address(0) - transferManagerSelectorForCollection[collection] = address(0); - - emit CollectionTransferManagerRemoved(collection); - } - - /** - * @notice Check the transfer manager for a token - * @param collection collection address - * @dev Support for ERC165 interface is checked AFTER custom implementation - */ - function checkTransferManagerForToken(address collection) external view override returns (address transferManager) { - // Assign transfer manager (if any) - transferManager = transferManagerSelectorForCollection[collection]; - - if (transferManager == address(0)) { - if (IERC165(collection).supportsInterface(INTERFACE_ID_ERC721)) { - transferManager = TRANSFER_MANAGER_ERC721; - } else if (IERC165(collection).supportsInterface(INTERFACE_ID_ERC1155)) { - transferManager = TRANSFER_MANAGER_ERC1155; - } - } - - return transferManager; - } -} diff --git a/contracts/authorizationManager/AuthenticatedProxy.sol b/contracts/authorizationManager/AuthenticatedProxy.sol new file mode 100644 index 0000000..5226fd4 --- /dev/null +++ b/contracts/authorizationManager/AuthenticatedProxy.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.8.9; + +import {IAuthenticatedProxy} from "../interfaces/IAuthenticatedProxy.sol"; +import {IAuthorizationManager} from "../interfaces/IAuthorizationManager.sol"; +import {IWETH} from "../interfaces/IWETH.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract AuthenticatedProxy is IAuthenticatedProxy { + address public immutable owner; + IAuthorizationManager public immutable authorizationManager; + address public immutable WETH; + bool public revoked; + + event Revoked(bool revoked); + + modifier onlyOwnerOrAuthed() { + require( + msg.sender == owner || + (!revoked && !authorizationManager.revoked() && msg.sender == authorizationManager.authorizedAddress()), + "Proxy: permission denied" + ); + _; + } + + constructor( + address _owner, + address _authorizationManager, + address _WETH + ) { + owner = _owner; + authorizationManager = IAuthorizationManager(_authorizationManager); + WETH = _WETH; + } + + function setRevoke(bool revoke) external override { + require(msg.sender == owner, "Proxy: permission denied"); + revoked = revoke; + emit Revoked(revoke); + } + + function safeTransfer( + address token, + address to, + uint256 amount + ) external override onlyOwnerOrAuthed { + require(IERC20(token).transferFrom(owner, to, amount), "Proxy: transfer failed"); + } + + function withdrawETH() external override onlyOwnerOrAuthed { + uint256 amount = IWETH(WETH).balanceOf(address(this)); + IWETH(WETH).withdraw(amount); + (bool success, ) = owner.call{value: amount}(""); + require(success, "Proxy: withdraw ETH failed"); + } + + function withdrawToken(address token) external override onlyOwnerOrAuthed { + uint256 amount = IERC20(token).balanceOf(address(this)); + require(IERC20(token).transfer(owner, amount), "Proxy: withdraw token failed"); + } + + function delegatecall(address dest, bytes memory data) + external + override + onlyOwnerOrAuthed + returns (bool success, bytes memory returndata) + { + (success, returndata) = dest.delegatecall(data); + } + + receive() external payable { + require(msg.sender == address(WETH), "Receive not allowed"); + } +} diff --git a/contracts/authorizationManager/AuthorizationManager.sol b/contracts/authorizationManager/AuthorizationManager.sol new file mode 100644 index 0000000..6a8b32c --- /dev/null +++ b/contracts/authorizationManager/AuthorizationManager.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.8.9; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IAuthorizationManager, IAuthenticatedProxy} from "../interfaces/IAuthorizationManager.sol"; + +import {AuthenticatedProxy} from "./AuthenticatedProxy.sol"; + +contract AuthorizationManager is Ownable, IAuthorizationManager { + mapping(address => address) public override proxies; + address public immutable override authorizedAddress; + bool public override revoked; + address public immutable WETH; + + event Revoked(); + + constructor(address _WETH, address _authorizedAddress) { + WETH = _WETH; + authorizedAddress = _authorizedAddress; + } + + function revoke() external override onlyOwner { + revoked = true; + emit Revoked(); + } + + /** + * Register a proxy contract with this registry + * + * @dev Must be called by the user which the proxy is for, creates a new AuthenticatedProxy + * @return proxy New AuthenticatedProxy contract + */ + function registerProxy() external override returns (address) { + return _registerProxyFor(msg.sender); + } + + function _registerProxyFor(address user) internal returns (address) { + require(address(proxies[user]) == address(0), "Authorization: user already has a proxy"); + address proxy = address(new AuthenticatedProxy(user, address(this), WETH)); + proxies[user] = proxy; + return proxy; + } +} diff --git a/contracts/executionStrategies/StrategyAnyItemFromCollectionForFixedPrice.sol b/contracts/executionStrategies/StrategyAnyItemFromCollectionForFixedPrice.sol index 3fd5416..69118f5 100644 --- a/contracts/executionStrategies/StrategyAnyItemFromCollectionForFixedPrice.sol +++ b/contracts/executionStrategies/StrategyAnyItemFromCollectionForFixedPrice.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {OrderTypes} from "../libraries/OrderTypes.sol"; import {IExecutionStrategy} from "../interfaces/IExecutionStrategy.sol"; diff --git a/contracts/executionStrategies/StrategyAnyItemInASetForFixedPrice.sol b/contracts/executionStrategies/StrategyAnyItemInASetForFixedPrice.sol index 62dfed8..234b113 100644 --- a/contracts/executionStrategies/StrategyAnyItemInASetForFixedPrice.sol +++ b/contracts/executionStrategies/StrategyAnyItemInASetForFixedPrice.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import {OrderTypes} from "../libraries/OrderTypes.sol"; diff --git a/contracts/executionStrategies/StrategyDutchAuction.sol b/contracts/executionStrategies/StrategyDutchAuction.sol index 98a18fe..330cfb9 100644 --- a/contracts/executionStrategies/StrategyDutchAuction.sol +++ b/contracts/executionStrategies/StrategyDutchAuction.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {OrderTypes} from "../libraries/OrderTypes.sol"; @@ -24,7 +24,7 @@ contract StrategyDutchAuction is IExecutionStrategy, Ownable { * @param _minimumAuctionLengthInSeconds minimum auction length in seconds */ constructor(uint256 _protocolFee, uint256 _minimumAuctionLengthInSeconds) { - require(_minimumAuctionLengthInSeconds >= 15 minutes, "Owner: Auction length must be > 15 min"); + require(_minimumAuctionLengthInSeconds >= 15 minutes, "Owner: auction length must be > 15 min"); PROTOCOL_FEE = _protocolFee; minimumAuctionLengthInSeconds = _minimumAuctionLengthInSeconds; @@ -53,8 +53,8 @@ contract StrategyDutchAuction is IExecutionStrategy, Ownable { uint256 endTime = makerAsk.endTime; // Underflow checks and auction length check - require(endTime >= (startTime + minimumAuctionLengthInSeconds), "Dutch Auction: Length must be longer"); - require(startPrice > endPrice, "Dutch Auction: Start price must be greater than end price"); + require(endTime >= (startTime + minimumAuctionLengthInSeconds), "Dutch Auction: length must be longer"); + require(startPrice > endPrice, "Dutch Auction: start price must be greater than end price"); uint256 currentAuctionPrice = startPrice - (((startPrice - endPrice) * (block.timestamp - startTime)) / (endTime - startTime)); @@ -101,7 +101,7 @@ contract StrategyDutchAuction is IExecutionStrategy, Ownable { * @dev It protects against auctions that would be too short to be executed (e.g., 15 seconds) */ function updateMinimumAuctionLength(uint256 _minimumAuctionLengthInSeconds) external onlyOwner { - require(_minimumAuctionLengthInSeconds >= 15 minutes, "Owner: Auction length must be > 15 min"); + require(_minimumAuctionLengthInSeconds >= 15 minutes, "Owner: auction length must be > 15 min"); minimumAuctionLengthInSeconds = _minimumAuctionLengthInSeconds; emit NewMinimumAuctionLengthInSeconds(_minimumAuctionLengthInSeconds); diff --git a/contracts/executionStrategies/StrategyPrivateSale.sol b/contracts/executionStrategies/StrategyPrivateSale.sol index 0192fbf..9e05949 100644 --- a/contracts/executionStrategies/StrategyPrivateSale.sol +++ b/contracts/executionStrategies/StrategyPrivateSale.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {OrderTypes} from "../libraries/OrderTypes.sol"; import {IExecutionStrategy} from "../interfaces/IExecutionStrategy.sol"; diff --git a/contracts/executionStrategies/StrategyStandardSaleForFixedPrice.sol b/contracts/executionStrategies/StrategyStandardSaleForFixedPrice.sol index 1e9aee7..26c4432 100644 --- a/contracts/executionStrategies/StrategyStandardSaleForFixedPrice.sol +++ b/contracts/executionStrategies/StrategyStandardSaleForFixedPrice.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {OrderTypes} from "../libraries/OrderTypes.sol"; import {IExecutionStrategy} from "../interfaces/IExecutionStrategy.sol"; diff --git a/contracts/interceptors/RedeemNFT.sol b/contracts/interceptors/RedeemNFT.sol new file mode 100644 index 0000000..90b10a3 --- /dev/null +++ b/contracts/interceptors/RedeemNFT.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IInterceptor, IERC721, OrderTypes} from "../interfaces/IInterceptor.sol"; +import {ILendPool, ILendPoolAddressesProvider} from "./interfaces/ILendPoolAddressesProvider.sol"; +import {IBNFT} from "./interfaces/IBNFT.sol"; + +contract RedeemNFT is IInterceptor { + using SafeERC20 for IERC20; + uint256 internal constant PERCENTAGE_FACTOR = 1e4; //percentage plus two decimals + uint256 internal constant HALF_PERCENT = PERCENTAGE_FACTOR / 2; + + struct LocalVars { + uint256 loanId; + uint256 bidFine; + uint256 totalDebt; + address tokenRepaid; + ILendPool lendPool; + } + + function beforeCollectionTransfer( + address token, + address from, + address, + uint256 tokenId, + uint256, + bytes memory extra + ) external override returns (bool) { + // do nothing if own token + if (IERC721(token).ownerOf(tokenId) == from) { + return true; + } + LocalVars memory vars; + // else redeem from bend lend pool + vars.lendPool = ILendPool(ILendPoolAddressesProvider(abi.decode(extra, (address))).getLendPool()); + // check bnft and nft ownership + { + ILendPool.NftData memory nftData = vars.lendPool.getNftData(token); + require(IERC721(token).ownerOf(tokenId) == nftData.bNftAddress, "Interceptor: no BNFT"); + require(IBNFT(nftData.bNftAddress).ownerOf(tokenId) == from, "Interceptor: not BNFT owner"); + } + // check token repaid + (vars.loanId, , , , vars.bidFine) = vars.lendPool.getNftAuctionData(token, tokenId); + (, vars.tokenRepaid, , vars.totalDebt, , ) = vars.lendPool.getNftDebtData(token, tokenId); + + require( + vars.totalDebt + vars.bidFine < IERC20(vars.tokenRepaid).balanceOf(address(this)), + "Interceptor: insufficent to repay debt" + ); + + // approve lend pool + IERC20(vars.tokenRepaid).safeApprove(address(vars.lendPool), vars.totalDebt + vars.bidFine); + // repay debt, will failed if debt greater than sell price + if (vars.bidFine > 0) { + // maxinmum debt repay amount 90% + uint256 redeemAmount = (vars.totalDebt * 9000 + HALF_PERCENT) / PERCENTAGE_FACTOR; + vars.lendPool.redeem(token, tokenId, redeemAmount, vars.bidFine); + vars.totalDebt -= redeemAmount; + } + vars.lendPool.repay(token, tokenId, vars.totalDebt); + // reset approve + IERC20(vars.tokenRepaid).safeApprove(address(vars.lendPool), 0); + + return IERC721(token).ownerOf(tokenId) == from; + } +} diff --git a/contracts/interceptors/interfaces/IBNFT.sol b/contracts/interceptors/interfaces/IBNFT.sol new file mode 100644 index 0000000..d46daa1 --- /dev/null +++ b/contracts/interceptors/interfaces/IBNFT.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.8.9; + +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; + +interface IBNFT is IERC721, IERC721Receiver { + function mint(address to, uint256 tokenId) external; + + function burn(uint256 tokenId) external; + + function underlyingAsset() external view returns (address); +} diff --git a/contracts/interceptors/interfaces/ILendPool.sol b/contracts/interceptors/interfaces/ILendPool.sol new file mode 100644 index 0000000..db425a6 --- /dev/null +++ b/contracts/interceptors/interfaces/ILendPool.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.8.9; + +interface ILendPool { + struct NftConfigurationMap { + //bit 0-15: LTV + //bit 16-31: Liq. threshold + //bit 32-47: Liq. bonus + //bit 56: NFT is active + //bit 57: NFT is frozen + uint256 data; + } + struct NftData { + //stores the nft configuration + NftConfigurationMap configuration; + //address of the bNFT contract + address bNftAddress; + //the id of the nft. Represents the position in the list of the active nfts + uint8 id; + } + + /** + * @dev Allows users to borrow a specific `amount` of the reserve underlying asset, provided that the borrower + * already deposited enough collateral + * - E.g. User borrows 100 USDC, receiving the 100 USDC in his wallet + * and lock collateral asset in contract + * @param reserveAsset The address of the underlying asset to borrow + * @param amount The amount to be borrowed + * @param nftAsset The address of the underlying NFT used as collateral + * @param nftTokenId The token ID of the underlying NFT used as collateral + * @param onBehalfOf Address of the user who will receive the loan. Should be the address of the borrower itself + * calling the function if he wants to borrow against his own collateral, or the address of the credit delegator + * if he has been given credit delegation allowance + * @param referralCode Code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + **/ + function borrow( + address reserveAsset, + uint256 amount, + address nftAsset, + uint256 nftTokenId, + address onBehalfOf, + uint16 referralCode + ) external; + + /** + * @notice Repays a borrowed `amount` on a specific reserve, burning the equivalent loan owned + * - E.g. User repays 100 USDC, burning loan and receives collateral asset + * @param nftAsset The address of the underlying NFT used as collateral + * @param nftTokenId The token ID of the underlying NFT used as collateral + * @param amount The amount to repay + * @return The final amount repaid, loan is burned or not + **/ + function repay( + address nftAsset, + uint256 nftTokenId, + uint256 amount + ) external returns (uint256, bool); + + /** + * @notice Redeem a NFT loan which state is in Auction + * - E.g. User repays 100 USDC, burning loan and receives collateral asset + * @param nftAsset The address of the underlying NFT used as collateral + * @param nftTokenId The token ID of the underlying NFT used as collateral + * @param amount The amount to repay the debt + * @param bidFine The amount of bid fine + **/ + function redeem( + address nftAsset, + uint256 nftTokenId, + uint256 amount, + uint256 bidFine + ) external returns (uint256); + + function getNftData(address asset) external view returns (NftData memory); + + /** + * @dev Returns the debt data of the NFT + * @param nftAsset The address of the NFT + * @param nftTokenId The token id of the NFT + * @return loanId the loan id of the NFT + * @return reserveAsset the address of the Reserve + * @return totalCollateral the total power of the NFT + * @return totalDebt the total debt of the NFT + * @return availableBorrows the borrowing power left of the NFT + * @return healthFactor the current health factor of the NFT + **/ + function getNftDebtData(address nftAsset, uint256 nftTokenId) + external + returns ( + uint256 loanId, + address reserveAsset, + uint256 totalCollateral, + uint256 totalDebt, + uint256 availableBorrows, + uint256 healthFactor + ); + + /** + * @dev Returns the auction data of the NFT + * @param nftAsset The address of the NFT + * @param nftTokenId The token id of the NFT + * @return loanId the loan id of the NFT + * @return bidderAddress the highest bidder address of the loan + * @return bidPrice the highest bid price in Reserve of the loan + * @return bidBorrowAmount the borrow amount in Reserve of the loan + * @return bidFine the penalty fine of the loan + **/ + function getNftAuctionData(address nftAsset, uint256 nftTokenId) + external + view + returns ( + uint256 loanId, + address bidderAddress, + uint256 bidPrice, + uint256 bidBorrowAmount, + uint256 bidFine + ); +} diff --git a/contracts/interceptors/interfaces/ILendPoolAddressesProvider.sol b/contracts/interceptors/interfaces/ILendPoolAddressesProvider.sol new file mode 100644 index 0000000..b07493e --- /dev/null +++ b/contracts/interceptors/interfaces/ILendPoolAddressesProvider.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.8.9; + +import {ILendPool} from "./ILendPool.sol"; + +/** + * @title LendPoolAddressesProvider contract + * @dev Main registry of addresses part of or connected to the protocol, including permissioned roles + * - Acting also as factory of proxies and admin of those, so with right to change its implementations + * - Owned by the Bend Governance + * @author Bend + **/ +interface ILendPoolAddressesProvider { + function getLendPool() external view returns (address); +} diff --git a/contracts/interfaces/IAuthenticatedProxy.sol b/contracts/interfaces/IAuthenticatedProxy.sol new file mode 100644 index 0000000..1549c70 --- /dev/null +++ b/contracts/interfaces/IAuthenticatedProxy.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +interface IAuthenticatedProxy { + function setRevoke(bool revoke) external; + + function safeTransfer( + address token, + address to, + uint256 amount + ) external; + + function withdrawETH() external; + + function withdrawToken(address token) external; + + function delegatecall(address dest, bytes memory data) external returns (bool, bytes memory); +} diff --git a/contracts/interfaces/IAuthorizationManager.sol b/contracts/interfaces/IAuthorizationManager.sol new file mode 100644 index 0000000..ee068d6 --- /dev/null +++ b/contracts/interfaces/IAuthorizationManager.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +import {IAuthenticatedProxy} from "./IAuthenticatedProxy.sol"; + +interface IAuthorizationManager { + function revoked() external returns (bool); + + function authorizedAddress() external returns (address); + + function proxies(address owner) external returns (address); + + function revoke() external; + + function registerProxy() external returns (address); +} diff --git a/contracts/interfaces/ILooksRareExchange.sol b/contracts/interfaces/IBendExchange.sol similarity index 90% rename from contracts/interfaces/ILooksRareExchange.sol rename to contracts/interfaces/IBendExchange.sol index a718177..be72359 100644 --- a/contracts/interfaces/ILooksRareExchange.sol +++ b/contracts/interfaces/IBendExchange.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {OrderTypes} from "../libraries/OrderTypes.sol"; -interface ILooksRareExchange { +interface IBendExchange { function matchAskWithTakerBidUsingETHAndWETH( OrderTypes.TakerOrder calldata takerBid, OrderTypes.MakerOrder calldata makerAsk diff --git a/contracts/interfaces/ICurrencyManager.sol b/contracts/interfaces/ICurrencyManager.sol index 043ecd2..749812e 100644 --- a/contracts/interfaces/ICurrencyManager.sol +++ b/contracts/interfaces/ICurrencyManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; interface ICurrencyManager { function addCurrency(address currency) external; diff --git a/contracts/interfaces/IExecutionManager.sol b/contracts/interfaces/IExecutionManager.sol index a790947..41c1479 100644 --- a/contracts/interfaces/IExecutionManager.sol +++ b/contracts/interfaces/IExecutionManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; interface IExecutionManager { function addStrategy(address strategy) external; diff --git a/contracts/interfaces/IExecutionStrategy.sol b/contracts/interfaces/IExecutionStrategy.sol index ace4b68..c344f2a 100644 --- a/contracts/interfaces/IExecutionStrategy.sol +++ b/contracts/interfaces/IExecutionStrategy.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {OrderTypes} from "../libraries/OrderTypes.sol"; diff --git a/contracts/interfaces/IInterceptor.sol b/contracts/interfaces/IInterceptor.sol new file mode 100644 index 0000000..a403b5c --- /dev/null +++ b/contracts/interfaces/IInterceptor.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {OrderTypes} from "../libraries/OrderTypes.sol"; + +interface IInterceptor { + function beforeCollectionTransfer( + address token, + address from, + address to, + uint256 tokenId, + uint256 amount, + bytes memory extra + ) external returns (bool); +} diff --git a/contracts/interfaces/IInterceptorManager.sol b/contracts/interfaces/IInterceptorManager.sol new file mode 100644 index 0000000..5dc2f5f --- /dev/null +++ b/contracts/interfaces/IInterceptorManager.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +interface IInterceptorManager { + function addCollectionInterceptor(address Interceptor) external; + + function removeCollectionInterceptor(address Interceptor) external; + + function isInterceptorWhitelisted(address Interceptor) external view returns (bool); + + function viewWhitelistedInterceptors(uint256 cursor, uint256 size) + external + view + returns (address[] memory, uint256); + + function viewCountWhitelistedInterceptors() external view returns (uint256); +} diff --git a/contracts/interfaces/IOwnable.sol b/contracts/interfaces/IOwnable.sol index 4a29c07..ba6114d 100644 --- a/contracts/interfaces/IOwnable.sol +++ b/contracts/interfaces/IOwnable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; interface IOwnable { function transferOwnership(address newOwner) external; diff --git a/contracts/interfaces/IRoyaltyFeeManager.sol b/contracts/interfaces/IRoyaltyFeeManager.sol index f8bdc59..a438f86 100644 --- a/contracts/interfaces/IRoyaltyFeeManager.sol +++ b/contracts/interfaces/IRoyaltyFeeManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; interface IRoyaltyFeeManager { function calculateRoyaltyFeeAndGetRecipient( diff --git a/contracts/interfaces/IRoyaltyFeeRegistry.sol b/contracts/interfaces/IRoyaltyFeeRegistry.sol index c666e5e..d40a810 100644 --- a/contracts/interfaces/IRoyaltyFeeRegistry.sol +++ b/contracts/interfaces/IRoyaltyFeeRegistry.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; interface IRoyaltyFeeRegistry { function updateRoyaltyInfoForCollection( diff --git a/contracts/interfaces/ITransfer.sol b/contracts/interfaces/ITransfer.sol new file mode 100644 index 0000000..4cd0358 --- /dev/null +++ b/contracts/interfaces/ITransfer.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +interface ITransfer { + function transferNonFungibleToken( + address token, + address from, + address to, + uint256 tokenId, + uint256 amount + ) external returns (bool); +} diff --git a/contracts/interfaces/ITransferManager.sol b/contracts/interfaces/ITransferManager.sol new file mode 100644 index 0000000..5e11d56 --- /dev/null +++ b/contracts/interfaces/ITransferManager.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +interface ITransferManager { + function transfers(address collection) external view returns (address); + + function addCollectionTransfer(address collection, address transfer) external; + + function removeCollectionTransfer(address collection) external; + + function checkTransferForToken(address collection) external view returns (address); +} diff --git a/contracts/interfaces/ITransferManagerNFT.sol b/contracts/interfaces/ITransferManagerNFT.sol deleted file mode 100644 index 4aeb8e2..0000000 --- a/contracts/interfaces/ITransferManagerNFT.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -interface ITransferManagerNFT { - function transferNonFungibleToken( - address collection, - address from, - address to, - uint256 tokenId, - uint256 amount - ) external; -} diff --git a/contracts/interfaces/ITransferSelectorNFT.sol b/contracts/interfaces/ITransferSelectorNFT.sol deleted file mode 100644 index 2022b6b..0000000 --- a/contracts/interfaces/ITransferSelectorNFT.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -interface ITransferSelectorNFT { - function checkTransferManagerForToken(address collection) external view returns (address); -} diff --git a/contracts/interfaces/IWETH.sol b/contracts/interfaces/IWETH.sol index d0c981e..6a0f206 100644 --- a/contracts/interfaces/IWETH.sol +++ b/contracts/interfaces/IWETH.sol @@ -2,6 +2,8 @@ pragma solidity >=0.5.0; interface IWETH { + function balanceOf(address) external view returns (uint256); + function deposit() external payable; function transfer(address to, uint256 value) external returns (bool); diff --git a/contracts/libraries/OrderTypes.sol b/contracts/libraries/OrderTypes.sol index 943bfec..4149f96 100644 --- a/contracts/libraries/OrderTypes.sol +++ b/contracts/libraries/OrderTypes.sol @@ -1,60 +1,70 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; /** * @title OrderTypes - * @notice This library contains order types for the LooksRare exchange. + * @notice This library contains order types for the Bend exchange. */ library OrderTypes { - // keccak256("MakerOrder(bool isOrderAsk,address signer,address collection,uint256 price,uint256 tokenId,uint256 amount,address strategy,address currency,uint256 nonce,uint256 startTime,uint256 endTime,uint256 minPercentageToAsk,bytes params)") - bytes32 internal constant MAKER_ORDER_HASH = 0x40261ade532fa1d2c7293df30aaadb9b3c616fae525a0b56d3d411c841a85028; + // keccak256("MakerOrder(bool isOrderAsk,address maker,address collection,uint256 price,uint256 tokenId,uint256 amount,address strategy,address currency,uint256 nonce,uint256 startTime,uint256 endTime,uint256 minPercentageToAsk,bytes params,address interceptor,bytes interceptorExtra)") + bytes32 internal constant MAKER_ORDER_HASH = 0xfd561ac528d7d2fc669c32105ec4867617451ed5ca6ccde2e4ed234a0a41010a; struct MakerOrder { bool isOrderAsk; // true --> ask / false --> bid - address signer; // signer of the maker order + address maker; // maker of the maker order address collection; // collection address uint256 price; // price (used as ) uint256 tokenId; // id of the token uint256 amount; // amount of tokens to sell/purchase (must be 1 for ERC721, 1+ for ERC1155) - address strategy; // strategy for trade execution (e.g., DutchAuction, StandardSaleForFixedPrice) - address currency; // currency (e.g., WETH) + address strategy; // strategy for trade execution (e.g. StandardSaleForFixedPrice) + address currency; uint256 nonce; // order nonce (must be unique unless new maker order is meant to override existing one e.g., lower ask price) uint256 startTime; // startTime in timestamp uint256 endTime; // endTime in timestamp - uint256 minPercentageToAsk; // slippage protection (9000 --> 90% of the final price must return to ask) + uint256 minPercentageToAsk; // slippage protection bytes params; // additional parameters uint8 v; // v: parameter (27 or 28) bytes32 r; // r: parameter bytes32 s; // s: parameter + address interceptor; + bytes interceptorExtra; } struct TakerOrder { bool isOrderAsk; // true --> ask / false --> bid address taker; // msg.sender uint256 price; // final price for the purchase - uint256 tokenId; - uint256 minPercentageToAsk; // // slippage protection (9000 --> 90% of the final price must return to ask) + uint256 tokenId; // id of the token + uint256 minPercentageToAsk; // // slippage protection bytes params; // other params (e.g., tokenId) + address interceptor; + bytes interceptorExtra; } function hash(MakerOrder memory makerOrder) internal pure returns (bytes32) { return keccak256( - abi.encode( - MAKER_ORDER_HASH, - makerOrder.isOrderAsk, - makerOrder.signer, - makerOrder.collection, - makerOrder.price, - makerOrder.tokenId, - makerOrder.amount, - makerOrder.strategy, - makerOrder.currency, - makerOrder.nonce, - makerOrder.startTime, - makerOrder.endTime, - makerOrder.minPercentageToAsk, - keccak256(makerOrder.params) + bytes.concat( + abi.encode( + MAKER_ORDER_HASH, + makerOrder.isOrderAsk, + makerOrder.maker, + makerOrder.collection, + makerOrder.price, + makerOrder.tokenId, + makerOrder.amount, + makerOrder.strategy, + makerOrder.currency, + makerOrder.nonce, + makerOrder.startTime, + makerOrder.endTime, + makerOrder.minPercentageToAsk + ), + abi.encode( + keccak256(makerOrder.params), + makerOrder.interceptor, + keccak256(makerOrder.interceptorExtra) + ) ) ); } diff --git a/contracts/libraries/SafeProxy.sol b/contracts/libraries/SafeProxy.sol new file mode 100644 index 0000000..a606d78 --- /dev/null +++ b/contracts/libraries/SafeProxy.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {ITransfer} from "../interfaces/ITransfer.sol"; +import {IInterceptor} from "../interfaces/IInterceptor.sol"; +import {IAuthenticatedProxy} from "../interfaces/IAuthenticatedProxy.sol"; +import {OrderTypes} from "../libraries/OrderTypes.sol"; + +library SafeProxy { + function safeTransferNonFungibleTokenFrom( + IAuthenticatedProxy proxy, + address transfer, + address interceptor, + address token, + address from, + address to, + uint256 tokenId, + uint256 amount, + bytes memory extra + ) internal { + if (interceptor != address(0)) { + safeDelegateCall( + proxy, + interceptor, + abi.encodeWithSelector( + IInterceptor(interceptor).beforeCollectionTransfer.selector, + token, + from, + to, + tokenId, + amount, + extra + ), + "SafeProxy: before transfer did not succeed" + ); + } + safeDelegateCall( + proxy, + transfer, + abi.encodeWithSelector( + ITransfer(transfer).transferNonFungibleToken.selector, + token, + from, + to, + tokenId, + amount + ), + "SafeProxy: transfer did not succeed" + ); + } + + function safeDelegateCall( + IAuthenticatedProxy proxy, + address target, + bytes memory data, + string memory errorMessage + ) internal { + (bool success, bytes memory returndata) = proxy.delegatecall(target, data); + _verifyCallResult(success, returndata, "SafeProxy: low-level delegate call failed"); + + if (returndata.length > 0) { + // Return data is optional + require(abi.decode(returndata, (bool)), errorMessage); + } + } + + function _verifyCallResult( + bool success, + bytes memory returndata, + string memory errorMessage + ) internal pure returns (bytes memory) { + if (success) { + return returndata; + } else { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } + } +} diff --git a/contracts/libraries/SignatureChecker.sol b/contracts/libraries/SignatureChecker.sol index d711315..ec02a37 100644 --- a/contracts/libraries/SignatureChecker.sol +++ b/contracts/libraries/SignatureChecker.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; @@ -26,14 +26,14 @@ library SignatureChecker { // https://crypto.iacr.org/2019/affevents/wac/medias/Heninger-BiasedNonceSense.pdf require( uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, - "Signature: Invalid s parameter" + "Signature: invalid s parameter" ); - require(v == 27 || v == 28, "Signature: Invalid v parameter"); + require(v == 27 || v == 28, "Signature: invalid v parameter"); // If the signature is valid (and not malleable), return the signer address address signer = ecrecover(hash, v, r, s); - require(signer != address(0), "Signature: Invalid signer"); + require(signer != address(0), "Signature: invalid signer"); return signer; } diff --git a/contracts/royaltyFeeHelpers/RoyaltyFeeRegistry.sol b/contracts/royaltyFeeHelpers/RoyaltyFeeRegistry.sol index a13b946..1847f4c 100644 --- a/contracts/royaltyFeeHelpers/RoyaltyFeeRegistry.sol +++ b/contracts/royaltyFeeHelpers/RoyaltyFeeRegistry.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; @@ -7,7 +7,7 @@ import {IRoyaltyFeeRegistry} from "../interfaces/IRoyaltyFeeRegistry.sol"; /** * @title RoyaltyFeeRegistry - * @notice It is a royalty fee registry for the LooksRare exchange. + * @notice It is a royalty fee registry for the Bend exchange. */ contract RoyaltyFeeRegistry is IRoyaltyFeeRegistry, Ownable { struct FeeInfo { @@ -29,7 +29,7 @@ contract RoyaltyFeeRegistry is IRoyaltyFeeRegistry, Ownable { * @param _royaltyFeeLimit new royalty fee limit (500 = 5%, 1,000 = 10%) */ constructor(uint256 _royaltyFeeLimit) { - require(_royaltyFeeLimit <= 9500, "Owner: Royalty fee limit too high"); + require(_royaltyFeeLimit <= 9500, "Owner: royalty fee limit too high"); royaltyFeeLimit = _royaltyFeeLimit; } @@ -38,7 +38,7 @@ contract RoyaltyFeeRegistry is IRoyaltyFeeRegistry, Ownable { * @param _royaltyFeeLimit new royalty fee limit (500 = 5%, 1,000 = 10%) */ function updateRoyaltyFeeLimit(uint256 _royaltyFeeLimit) external override onlyOwner { - require(_royaltyFeeLimit <= 9500, "Owner: Royalty fee limit too high"); + require(_royaltyFeeLimit <= 9500, "Owner: royalty fee limit too high"); royaltyFeeLimit = _royaltyFeeLimit; emit NewRoyaltyFeeLimit(_royaltyFeeLimit); @@ -57,7 +57,7 @@ contract RoyaltyFeeRegistry is IRoyaltyFeeRegistry, Ownable { address receiver, uint256 fee ) external override onlyOwner { - require(fee <= royaltyFeeLimit, "Registry: Royalty fee too high"); + require(fee <= royaltyFeeLimit, "Registry: royalty fee too high"); _royaltyFeeInfoCollection[collection] = FeeInfo({setter: setter, receiver: receiver, fee: fee}); emit RoyaltyFeeUpdate(collection, setter, receiver, fee); diff --git a/contracts/royaltyFeeHelpers/RoyaltyFeeSetter.sol b/contracts/royaltyFeeHelpers/RoyaltyFeeSetter.sol index a48f3a3..80e3bf0 100644 --- a/contracts/royaltyFeeHelpers/RoyaltyFeeSetter.sol +++ b/contracts/royaltyFeeHelpers/RoyaltyFeeSetter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; @@ -45,8 +45,8 @@ contract RoyaltyFeeSetter is Ownable { address receiver, uint256 fee ) external { - require(!IERC165(collection).supportsInterface(INTERFACE_ID_ERC2981), "Admin: Must not be ERC2981"); - require(msg.sender == IOwnable(collection).admin(), "Admin: Not the admin"); + require(!IERC165(collection).supportsInterface(INTERFACE_ID_ERC2981), "Admin: must not be ERC2981"); + require(msg.sender == IOwnable(collection).admin(), "Admin: not the admin"); _updateRoyaltyInfoForCollectionIfOwnerOrAdmin(collection, setter, receiver, fee); } @@ -65,8 +65,8 @@ contract RoyaltyFeeSetter is Ownable { address receiver, uint256 fee ) external { - require(!IERC165(collection).supportsInterface(INTERFACE_ID_ERC2981), "Owner: Must not be ERC2981"); - require(msg.sender == IOwnable(collection).owner(), "Owner: Not the owner"); + require(!IERC165(collection).supportsInterface(INTERFACE_ID_ERC2981), "Owner: must not be ERC2981"); + require(msg.sender == IOwnable(collection).owner(), "Owner: not the owner"); _updateRoyaltyInfoForCollectionIfOwnerOrAdmin(collection, setter, receiver, fee); } @@ -86,7 +86,7 @@ contract RoyaltyFeeSetter is Ownable { uint256 fee ) external { (address currentSetter, , ) = IRoyaltyFeeRegistry(royaltyFeeRegistry).royaltyFeeInfoCollection(collection); - require(msg.sender == currentSetter, "Setter: Not the setter"); + require(msg.sender == currentSetter, "Setter: not the setter"); IRoyaltyFeeRegistry(royaltyFeeRegistry).updateRoyaltyInfoForCollection(collection, setter, receiver, fee); } @@ -174,12 +174,12 @@ contract RoyaltyFeeSetter is Ownable { uint256 fee ) internal { (address currentSetter, , ) = IRoyaltyFeeRegistry(royaltyFeeRegistry).royaltyFeeInfoCollection(collection); - require(currentSetter == address(0), "Setter: Already set"); + require(currentSetter == address(0), "Setter: already set"); require( (IERC165(collection).supportsInterface(INTERFACE_ID_ERC721) || IERC165(collection).supportsInterface(INTERFACE_ID_ERC1155)), - "Setter: Not ERC721/ERC1155" + "Setter: not ERC721/ERC1155" ); IRoyaltyFeeRegistry(royaltyFeeRegistry).updateRoyaltyInfoForCollection(collection, setter, receiver, fee); diff --git a/contracts/test/DutchAuction.t.sol b/contracts/test/DutchAuction.t.sol deleted file mode 100644 index 1d76de7..0000000 --- a/contracts/test/DutchAuction.t.sol +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.7; - -import {OrderTypes, StrategyDutchAuction} from "../executionStrategies/StrategyDutchAuction.sol"; -import {TestHelpers} from "./TestHelpers.sol"; - -abstract contract TestParameters { - // All the parameters are dummy & used for compatibility with Maker/Taker but don't impact Dutch Auction - address internal _TAKER = address(1); - address internal _MAKER = address(2); - address internal _STRATEGY = address(3); - address internal _COLLECTION = address(4); - address internal _CURRENCY = address(5); - uint256 internal _AMOUNT = 1; - uint256 internal _NONCE = 0; - uint256 internal _TOKEN_ID = 1; - uint256 internal _MIN_PERCENTAGE_TO_ASK = 8500; - bytes internal _TAKER_PARAMS; - uint8 internal _V = 27; - bytes32 internal _R; - bytes32 internal _S; - - // Dutch Auction constructor parameters - uint256 internal _PROTOCOL_FEE = 200; - uint256 internal _MIN_AUCTION_LENGTH = 15 minutes; -} - -contract StrategyDutchAuctionTest is TestHelpers, TestParameters { - StrategyDutchAuction public strategyDutchAuction; - - function setUp() public { - strategyDutchAuction = new StrategyDutchAuction(_PROTOCOL_FEE, _MIN_AUCTION_LENGTH); - } - - function testTimeAndPriceUnderOverflow( - uint96 auctionLength, - uint256 startPrice, - uint256 endPrice - ) public { - cheats.assume(_MIN_AUCTION_LENGTH < uint256(auctionLength)); - cheats.assume(startPrice > endPrice); - - uint256 startTime = block.timestamp; - uint256 endTime = startTime + uint256(auctionLength); - bytes memory makerParams = abi.encode(startPrice); - - uint256 takerPrice = startPrice - - (((startPrice - endPrice) * (block.timestamp - startTime)) / (endTime - startTime)); - - OrderTypes.TakerOrder memory takerBidOrder = OrderTypes.TakerOrder( - false, - _TAKER, - takerPrice, - _TOKEN_ID, - _MIN_PERCENTAGE_TO_ASK, - _TAKER_PARAMS - ); - - OrderTypes.MakerOrder memory makerAskOrder = OrderTypes.MakerOrder( - true, - _MAKER, - _COLLECTION, - endPrice, - _TOKEN_ID, - _AMOUNT, - _STRATEGY, - _CURRENCY, - _NONCE, - startTime, - endTime, - _MIN_PERCENTAGE_TO_ASK, - makerParams, - _V, - _R, - _S - ); - - (bool canExecute, , ) = strategyDutchAuction.canExecuteTakerBid(takerBidOrder, makerAskOrder); - assert(canExecute); - } -} diff --git a/contracts/test/ICheatCodes.sol b/contracts/test/ICheatCodes.sol deleted file mode 100644 index 4ba0f23..0000000 --- a/contracts/test/ICheatCodes.sol +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -interface ICheatCodes { - // Set block.timestamp (newTimestamp) - function warp(uint256) external; - - // Set block.height (newHeight) - function roll(uint256) external; - - // Set block.basefee (newBasefee) - function fee(uint256) external; - - // Loads a storage slot from an address (who, slot) - function load(address, bytes32) external returns (bytes32); - - // Stores a value to an address' storage slot, (who, slot, value) - function store( - address, - bytes32, - bytes32 - ) external; - - // Signs data, (privateKey, digest) => (v, r, s) - function sign(uint256, bytes32) - external - returns ( - uint8, - bytes32, - bytes32 - ); - - // Gets address for a given private key, (privateKey) => (address) - function addr(uint256) external returns (address); - - // Performs a foreign function call via terminal, (stringInputs) => (result) - function ffi(string[] calldata) external returns (bytes memory); - - // Sets the *next* call's msg.sender to be the input address - function prank(address) external; - - // Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called - function startPrank(address) external; - - // Sets the *next* call's msg.sender to be the input address, and the tx.origin to be the second input - function prank(address, address) external; - - // Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called, and the tx.origin to be the second input - function startPrank(address, address) external; - - // Resets subsequent calls' msg.sender to be `address(this)` - function stopPrank() external; - - // Sets an address' balance, (who, newBalance) - function deal(address, uint256) external; - - // Sets an address' code, (who, newCode) - function etch(address, bytes calldata) external; - - // Expects an error on next call - function expectRevert(bytes calldata) external; - - function expectRevert(bytes4) external; - - // Record all storage reads and writes - function record() external; - - // Gets all accessed reads and write slot from a recording session, for a given address - function accesses(address) external returns (bytes32[] memory reads, bytes32[] memory writes); - - // Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData). - // Call this function, then emit an event, then call a function. Internally after the call, we check if - // logs were emitted in the expected order with the expected topics and data (as specified by the booleans) - function expectEmit( - bool, - bool, - bool, - bool - ) external; - - // Mocks a call to an address, returning specified data. - // Calldata can either be strict or a partial match, e.g. if you only - // pass a Solidity selector to the expected calldata, then the entire Solidity - // function will be mocked. - function mockCall( - address, - bytes calldata, - bytes calldata - ) external; - - // Clears all mocked calls - function clearMockedCalls() external; - - // Expect a call to an address with the specified calldata. - // Calldata can either be strict or a partial match - function expectCall(address, bytes calldata) external; - - // Fetches the contract bytecode from its artifact file - function getCode(string calldata) external returns (bytes memory); - - // Label an address in test traces - function label(address addr, string calldata label) external; - - // When fuzzing, generate new inputs if conditional not met - function assume(bool) external; -} diff --git a/contracts/test/MockBNFT.sol b/contracts/test/MockBNFT.sol new file mode 100644 index 0000000..9c39b20 --- /dev/null +++ b/contracts/test/MockBNFT.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.8.9; + +import {IBNFT, IERC721Receiver} from "../interceptors/interfaces/IBNFT.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {ERC721Enumerable, ERC721} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; + +contract MockBNFT is ERC721Enumerable, IBNFT { + address private _underlyingAsset; + + constructor( + string memory name_, + string memory symbol_, + address underlyingAsset_ + ) ERC721(name_, symbol_) { + _underlyingAsset = underlyingAsset_; + } + + function mint(address to, uint256 tokenId) external override { + _mint(to, tokenId); + } + + function burn(uint256 tokenId) external override { + IERC721(_underlyingAsset).safeTransferFrom(address(this), _msgSender(), tokenId); + _burn(tokenId); + } + + function underlyingAsset() external view override returns (address) { + return _underlyingAsset; + } + + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) external pure override returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } +} diff --git a/contracts/test/utils/MockERC1155.sol b/contracts/test/MockERC1155.sol similarity index 95% rename from contracts/test/utils/MockERC1155.sol rename to contracts/test/MockERC1155.sol index f68b860..6cec482 100644 --- a/contracts/test/utils/MockERC1155.sol +++ b/contracts/test/MockERC1155.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; diff --git a/contracts/test/utils/MockERC20.sol b/contracts/test/MockERC20.sol similarity index 92% rename from contracts/test/utils/MockERC20.sol rename to contracts/test/MockERC20.sol index 0f5c7bf..21f6a26 100644 --- a/contracts/test/utils/MockERC20.sol +++ b/contracts/test/MockERC20.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/contracts/test/utils/MockERC721.sol b/contracts/test/MockERC721.sol similarity index 93% rename from contracts/test/utils/MockERC721.sol rename to contracts/test/MockERC721.sol index d834c98..f748fc5 100644 --- a/contracts/test/utils/MockERC721.sol +++ b/contracts/test/MockERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; diff --git a/contracts/test/utils/MockERC721WithAdmin.sol b/contracts/test/MockERC721WithAdmin.sol similarity index 94% rename from contracts/test/utils/MockERC721WithAdmin.sol rename to contracts/test/MockERC721WithAdmin.sol index 7886a8d..bfe54de 100644 --- a/contracts/test/utils/MockERC721WithAdmin.sol +++ b/contracts/test/MockERC721WithAdmin.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; diff --git a/contracts/test/utils/MockERC721WithOwner.sol b/contracts/test/MockERC721WithOwner.sol similarity index 95% rename from contracts/test/utils/MockERC721WithOwner.sol rename to contracts/test/MockERC721WithOwner.sol index cf4b421..36cd8fc 100644 --- a/contracts/test/utils/MockERC721WithOwner.sol +++ b/contracts/test/MockERC721WithOwner.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; diff --git a/contracts/test/utils/MockERC721WithRoyalty.sol b/contracts/test/MockERC721WithRoyalty.sol similarity index 97% rename from contracts/test/utils/MockERC721WithRoyalty.sol rename to contracts/test/MockERC721WithRoyalty.sol index 5fe7d59..6ce7516 100644 --- a/contracts/test/utils/MockERC721WithRoyalty.sol +++ b/contracts/test/MockERC721WithRoyalty.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {IERC165, ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol"; diff --git a/contracts/test/MockInterceptor.sol b/contracts/test/MockInterceptor.sol new file mode 100644 index 0000000..0ee336f --- /dev/null +++ b/contracts/test/MockInterceptor.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +import {IInterceptor, IERC721, OrderTypes} from "../interfaces/IInterceptor.sol"; + +contract MockInterceptor is IInterceptor { + function beforeCollectionTransfer( + address, + address, + address, + uint256, + uint256, + bytes memory + ) external pure override returns (bool) { + return true; + } +} diff --git a/contracts/test/MockLendPool.sol b/contracts/test/MockLendPool.sol new file mode 100644 index 0000000..8ed38e0 --- /dev/null +++ b/contracts/test/MockLendPool.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.8.9; + +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {Counters} from "@openzeppelin/contracts/utils/Counters.sol"; + +import {ILendPool} from "../interceptors/interfaces/ILendPool.sol"; +import {IBNFT, IERC721Receiver} from "../interceptors/interfaces/IBNFT.sol"; + +import {MockBNFT} from "./MockBNFT.sol"; + +contract MockLendPool is ILendPool, IERC721Receiver { + struct LoanData { + address borrower; + address reserveAsset; + uint256 amount; + } + using SafeERC20 for IERC20; + mapping(address => IBNFT) private _mockedBnfts; + + using Counters for Counters.Counter; + Counters.Counter private _loanIdTracker; + + mapping(address => mapping(uint256 => uint256)) private _nftToLoanIds; + + mapping(address => mapping(uint256 => uint256)) private _mockBidFine; + mapping(uint256 => LoanData) private _loans; + + function onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes calldata data + ) external pure override returns (bytes4) { + operator; + from; + tokenId; + data; + return IERC721Receiver.onERC721Received.selector; + } + + function setMockInAuction( + address nftAsset, + uint256 nftTokenId, + uint256 bidFine + ) external { + _mockBidFine[nftAsset][nftTokenId] = bidFine; + } + + function borrow( + address reserveAsset, + uint256 amount, + address nftAsset, + uint256 nftTokenId, + address onBehalfOf, + uint16 + ) external { + uint256 loanId = _nftToLoanIds[nftAsset][nftTokenId]; + if (loanId > 0) { + require(_loans[loanId].amount == 0, "MockLendPool: can not borrow more at mock version"); + } else { + _loanIdTracker.increment(); + loanId = _loanIdTracker.current(); + _nftToLoanIds[nftAsset][nftTokenId] = loanId; + } + IBNFT bnft = _mockedBnfts[nftAsset]; + if (address(bnft) == address(0)) { + bnft = new MockBNFT("BNFT", "BNFT", nftAsset); + _mockedBnfts[nftAsset] = bnft; + } + IERC20(reserveAsset).safeTransfer(msg.sender, amount); + IERC721(nftAsset).safeTransferFrom(msg.sender, address(bnft), nftTokenId); + bnft.mint(onBehalfOf, nftTokenId); + LoanData storage loan = _loans[loanId]; + loan.borrower = onBehalfOf; + loan.reserveAsset = reserveAsset; + loan.amount = amount; + } + + function repay( + address nftAsset, + uint256 nftTokenId, + uint256 amount + ) external returns (uint256, bool) { + uint256 loanId = _nftToLoanIds[nftAsset][nftTokenId]; + IBNFT bnft = _mockedBnfts[nftAsset]; + LoanData storage loan = _loans[loanId]; + uint256 repayAmount = loan.amount; + if (amount < repayAmount) { + repayAmount = amount; + } + if (repayAmount == loan.amount) { + loan.amount = 0; + IERC20(loan.reserveAsset).safeTransferFrom(msg.sender, address(this), repayAmount); + bnft.burn(nftTokenId); + IERC721(nftAsset).safeTransferFrom(address(this), loan.borrower, nftTokenId); + return (repayAmount, true); + } else { + loan.amount -= repayAmount; + IERC20(loan.reserveAsset).safeTransferFrom(msg.sender, address(this), repayAmount); + return (repayAmount, false); + } + } + + function redeem( + address nftAsset, + uint256 nftTokenId, + uint256 amount, + uint256 bidFine + ) external returns (uint256) { + uint256 loanId = _nftToLoanIds[nftAsset][nftTokenId]; + LoanData storage loan = _loans[loanId]; + IERC20(loan.reserveAsset).safeTransferFrom(msg.sender, address(this), amount + bidFine); + loan.amount -= amount; + require(_mockBidFine[nftAsset][nftTokenId] == bidFine); + return 0; + } + + function getNftData(address asset) external view returns (NftData memory) { + NftData memory _nftData; + _nftData.bNftAddress = address(_mockedBnfts[asset]); + return _nftData; + } + + function getNftDebtData(address nftAsset, uint256 nftTokenId) + external + view + returns ( + uint256 loanId, + address reserveAsset, + uint256 totalCollateral, + uint256 totalDebt, + uint256 availableBorrows, + uint256 healthFactor + ) + { + loanId = _nftToLoanIds[nftAsset][nftTokenId]; + LoanData memory loan = _loans[loanId]; + return (loanId, loan.reserveAsset, 0, loan.amount, 0, 0); + } + + function getNftAuctionData(address nftAsset, uint256 nftTokenId) + external + view + returns ( + uint256 loanId, + address bidderAddress, + uint256 bidPrice, + uint256 bidBorrowAmount, + uint256 bidFine + ) + { + uint256 _loanId = _nftToLoanIds[nftAsset][nftTokenId]; + return (_loanId, address(0), 0, 0, _mockBidFine[nftAsset][nftTokenId]); + } +} diff --git a/contracts/test/MockLendPoolAddressesProvider.sol b/contracts/test/MockLendPoolAddressesProvider.sol new file mode 100644 index 0000000..f4f7a6c --- /dev/null +++ b/contracts/test/MockLendPoolAddressesProvider.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.8.9; + +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ILendPool} from "../interceptors/interfaces/ILendPool.sol"; +import {MockLendPool} from "./MockLendPool.sol"; +import {ILendPoolAddressesProvider} from "../interceptors/interfaces/ILendPoolAddressesProvider.sol"; + +contract MockLendPoolAddressesProvider is ILendPoolAddressesProvider { + ILendPool public pool; + + constructor() { + pool = new MockLendPool(); + } + + function getLendPool() external view returns (address) { + return address(pool); + } +} diff --git a/contracts/test/utils/MockNonCompliantERC721.sol b/contracts/test/MockNonCompliantERC721.sol similarity index 96% rename from contracts/test/utils/MockNonCompliantERC721.sol rename to contracts/test/MockNonCompliantERC721.sol index 44f0f74..fb39191 100644 --- a/contracts/test/utils/MockNonCompliantERC721.sol +++ b/contracts/test/MockNonCompliantERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; diff --git a/contracts/test/MockNonPayable.sol b/contracts/test/MockNonPayable.sol new file mode 100644 index 0000000..6dff1ff --- /dev/null +++ b/contracts/test/MockNonPayable.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.8.9; +import {IAuthorizationManager} from "../interfaces/IAuthorizationManager.sol"; + +contract MockNonPayable { + receive() external payable { + revert("Non payable"); + } + + function registerProxy(IAuthorizationManager manager) external returns (address) { + return address(manager.registerProxy()); + } +} diff --git a/contracts/test/MockNonTransferERC20.sol b/contracts/test/MockNonTransferERC20.sol new file mode 100644 index 0000000..af5862c --- /dev/null +++ b/contracts/test/MockNonTransferERC20.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockNonTransferERC20 is ERC20 { + constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) { + // + } + + function transfer(address, uint256) public pure override returns (bool) { + return false; + } + + function transferFrom( + address, + address, + uint256 + ) public pure override returns (bool) { + return false; + } +} diff --git a/contracts/test/utils/MockSignerContract.sol b/contracts/test/MockSignerContract.sol similarity index 89% rename from contracts/test/utils/MockSignerContract.sol rename to contracts/test/MockSignerContract.sol index 141d621..d005081 100644 --- a/contracts/test/utils/MockSignerContract.sol +++ b/contracts/test/MockSignerContract.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity 0.8.9; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; @@ -7,6 +7,8 @@ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IAuthorizationManager} from "../interfaces/IAuthorizationManager.sol"; + contract MockSignerContract is IERC1271, ERC721Holder, Ownable { // bytes4(keccak256("isValidSignature(bytes32,bytes)") bytes4 internal constant MAGICVALUE = 0x1626ba7e; @@ -39,6 +41,13 @@ contract MockSignerContract is IERC1271, ERC721Holder, Ownable { IERC721(collection).transferFrom(address(this), msg.sender, tokenId); } + /** + * @notice register authenticated proxy + */ + function registerProxy(IAuthorizationManager proxyManager) external onlyOwner { + proxyManager.registerProxy(); + } + /** * @notice Verifies that the signer is the owner of the signing contract. */ diff --git a/contracts/test/TestHelpers.sol b/contracts/test/TestHelpers.sol deleted file mode 100644 index d8bf103..0000000 --- a/contracts/test/TestHelpers.sol +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {ICheatCodes} from "./ICheatCodes.sol"; -import {DSTest} from "../../lib/ds-test/src/test.sol"; - -abstract contract TestHelpers is DSTest { - ICheatCodes public cheats = ICheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - - address public user1 = address(1); - address public user2 = address(2); - address public user3 = address(3); - address public user4 = address(4); - address public user5 = address(5); - address public user6 = address(6); - address public user7 = address(7); - address public user8 = address(8); - address public user9 = address(9); - - modifier asPrankedUser(address _user) { - cheats.startPrank(_user); - _; - cheats.stopPrank(); - } - - function assertQuasiEq(uint256 a, uint256 b) public { - require(a >= 1e18 || b >= 1e18, "Error: a & b must be > 1e18"); - - // 0.000001 % precision tolerance - uint256 PRECISION_LOSS = 1e9; - - if (a == b) { - assertEq(a, b); - } else if (a > b) { - assertGt(a, b); - assertLt(a - PRECISION_LOSS, b); - } else if (a < b) { - assertGt(a, b - PRECISION_LOSS); - assertLt(a, b); - } - } - - function _parseEther(uint256 value) internal pure returns (uint256) { - return value * 1e18; - } - - function _parseEtherWithFloating(uint256 value, uint8 floatingDigits) internal pure returns (uint256) { - assert(floatingDigits <= 18); - return value * (10**(18 - floatingDigits)); - } -} diff --git a/contracts/test/utils/WETH.sol b/contracts/test/WETH.sol similarity index 100% rename from contracts/test/utils/WETH.sol rename to contracts/test/WETH.sol diff --git a/contracts/transferManagers/TransferManagerERC1155.sol b/contracts/transferManagers/TransferManagerERC1155.sol deleted file mode 100644 index 66fc244..0000000 --- a/contracts/transferManagers/TransferManagerERC1155.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; - -import {ITransferManagerNFT} from "../interfaces/ITransferManagerNFT.sol"; - -/** - * @title TransferManagerERC1155 - * @notice It allows the transfer of ERC1155 tokens. - */ -contract TransferManagerERC1155 is ITransferManagerNFT { - address public immutable LOOKS_RARE_EXCHANGE; - - /** - * @notice Constructor - * @param _looksRareExchange address of the LooksRare exchange - */ - constructor(address _looksRareExchange) { - LOOKS_RARE_EXCHANGE = _looksRareExchange; - } - - /** - * @notice Transfer ERC1155 token(s) - * @param collection address of the collection - * @param from address of the sender - * @param to address of the recipient - * @param tokenId tokenId - * @param amount amount of tokens (1 and more for ERC1155) - */ - function transferNonFungibleToken( - address collection, - address from, - address to, - uint256 tokenId, - uint256 amount - ) external override { - require(msg.sender == LOOKS_RARE_EXCHANGE, "Transfer: Only LooksRare Exchange"); - // https://docs.openzeppelin.com/contracts/3.x/api/token/erc1155#IERC1155-safeTransferFrom-address-address-uint256-uint256-bytes- - IERC1155(collection).safeTransferFrom(from, to, tokenId, amount, ""); - } -} diff --git a/contracts/transferManagers/TransferManagerERC721.sol b/contracts/transferManagers/TransferManagerERC721.sol deleted file mode 100644 index 8409c4f..0000000 --- a/contracts/transferManagers/TransferManagerERC721.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; - -import {ITransferManagerNFT} from "../interfaces/ITransferManagerNFT.sol"; - -/** - * @title TransferManagerERC721 - * @notice It allows the transfer of ERC721 tokens. - */ -contract TransferManagerERC721 is ITransferManagerNFT { - address public immutable LOOKS_RARE_EXCHANGE; - - /** - * @notice Constructor - * @param _looksRareExchange address of the LooksRare exchange - */ - constructor(address _looksRareExchange) { - LOOKS_RARE_EXCHANGE = _looksRareExchange; - } - - /** - * @notice Transfer ERC721 token - * @param collection address of the collection - * @param from address of the sender - * @param to address of the recipient - * @param tokenId tokenId - * @dev For ERC721, amount is not used - */ - function transferNonFungibleToken( - address collection, - address from, - address to, - uint256 tokenId, - uint256 - ) external override { - require(msg.sender == LOOKS_RARE_EXCHANGE, "Transfer: Only LooksRare Exchange"); - // https://docs.openzeppelin.com/contracts/2.x/api/token/erc721#IERC721-safeTransferFrom - IERC721(collection).safeTransferFrom(from, to, tokenId); - } -} diff --git a/contracts/transferManagers/TransferManagerNonCompliantERC721.sol b/contracts/transferManagers/TransferManagerNonCompliantERC721.sol deleted file mode 100644 index e8b3223..0000000 --- a/contracts/transferManagers/TransferManagerNonCompliantERC721.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; -import {ITransferManagerNFT} from "../interfaces/ITransferManagerNFT.sol"; - -/** - * @title TransferManagerNonCompliantERC721 - * @notice It allows the transfer of ERC721 tokens without safeTransferFrom. - */ -contract TransferManagerNonCompliantERC721 is ITransferManagerNFT { - address public immutable LOOKS_RARE_EXCHANGE; - - /** - * @notice Constructor - * @param _looksRareExchange address of the LooksRare exchange - */ - constructor(address _looksRareExchange) { - LOOKS_RARE_EXCHANGE = _looksRareExchange; - } - - /** - * @notice Transfer ERC721 token - * @param collection address of the collection - * @param from address of the sender - * @param to address of the recipient - * @param tokenId tokenId - */ - function transferNonFungibleToken( - address collection, - address from, - address to, - uint256 tokenId, - uint256 - ) external override { - require(msg.sender == LOOKS_RARE_EXCHANGE, "Transfer: Only LooksRare Exchange"); - IERC721(collection).transferFrom(from, to, tokenId); - } -} diff --git a/contracts/transfers/TransferERC1155.sol b/contracts/transfers/TransferERC1155.sol new file mode 100644 index 0000000..2eb6a8f --- /dev/null +++ b/contracts/transfers/TransferERC1155.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +import {ITransfer, IERC1155} from "../interfaces/ITransfer.sol"; + +/** + * @title TransferERC1155 + * @notice It allows the transfer of ERC1155 tokens. + */ +contract TransferERC1155 is ITransfer { + function transferNonFungibleToken( + address token, + address from, + address to, + uint256 tokenId, + uint256 amount + ) external override returns (bool) { + // https://docs.openzeppelin.com/contracts/3.x/api/token/erc1155#IERC1155-safeTransferFrom-address-address-uint256-uint256-bytes- + IERC1155(token).safeTransferFrom(from, to, tokenId, amount, ""); + return true; + } +} diff --git a/contracts/transfers/TransferERC721.sol b/contracts/transfers/TransferERC721.sol new file mode 100644 index 0000000..9390b6b --- /dev/null +++ b/contracts/transfers/TransferERC721.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +import {ITransfer, IERC721} from "../interfaces/ITransfer.sol"; + +/** + * @title TransferERC721 + * @notice It allows the transfer of ERC721 tokens. + */ +contract TransferERC721 is ITransfer { + function transferNonFungibleToken( + address token, + address from, + address to, + uint256 tokenId, + uint256 + ) external override returns (bool) { + IERC721(token).safeTransferFrom(from, to, tokenId); + return true; + } +} diff --git a/contracts/transfers/TransferNonCompliantERC721.sol b/contracts/transfers/TransferNonCompliantERC721.sol new file mode 100644 index 0000000..d92266d --- /dev/null +++ b/contracts/transfers/TransferNonCompliantERC721.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +import {ITransfer, IERC721} from "../interfaces/ITransfer.sol"; + +/** + * @title TransferNonCompliantERC721 + * @notice It allows the transfer of ERC721 tokens without safeTransferFrom. + */ +contract TransferNonCompliantERC721 is ITransfer { + function transferNonFungibleToken( + address token, + address from, + address to, + uint256 tokenId, + uint256 + ) external override returns (bool) { + IERC721(token).transferFrom(from, to, tokenId); + return true; + } +} diff --git a/foundry.toml b/foundry.toml deleted file mode 100644 index ce0ac69..0000000 --- a/foundry.toml +++ /dev/null @@ -1,36 +0,0 @@ -[default] -auto_detect_solc = true -block_base_fee_per_gas = 0 -block_coinbase = '0x0000000000000000000000000000000000000000' -block_difficulty = 0 -block_number = 0 -block_timestamp = 0 -cache = true -cache_path = 'cache' -evm_version = 'london' -extra_output = [] -extra_output_files = [] -ffi = false -force = false -fuzz_max_global_rejects = 65536 -fuzz_max_local_rejects = 1024 -fuzz_runs = 1000 -gas_limit = 9223372036854775807 -gas_price = 0 -gas_reports = ['*'] -ignored_error_codes = [1878] -initial_balance = '0xffffffffffffffffffffffff' -libraries = [] -libs = ['node_modules'] -names = false -offline = false -optimizer = true -optimizer_runs = 888888 -out = 'artifacts' -sender = '0x00a329c0648769a73afac7f9381e08fb43dbea72' -sizes = false -src = 'contracts' -test = 'test' -tx_origin = '0x00a329c0648769a73afac7f9381e08fb43dbea72' -verbosity = 0 -via_ir = false diff --git a/hardhat.config.ts b/hardhat.config.ts index 2b3e9c4..1e4f954 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -18,13 +18,7 @@ const config: HardhatUserConfig = { defaultNetwork: "hardhat", networks: { hardhat: { - allowUnlimitedContractSize: false, - hardfork: "berlin", // Berlin is used (temporarily) to avoid issues with coverage - mining: { - auto: true, - interval: 50000, - }, - gasPrice: "auto", + initialBaseFeePerGas: 0, }, }, etherscan: { @@ -33,7 +27,7 @@ const config: HardhatUserConfig = { solidity: { compilers: [ { - version: "0.8.7", + version: "0.8.9", settings: { optimizer: { enabled: true, runs: 888888 } }, }, { diff --git a/lib/ds-test b/lib/ds-test deleted file mode 160000 index 0a5da56..0000000 --- a/lib/ds-test +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0a5da56b0d65960e6a994d2ec8245e6edd38c248 diff --git a/package.json b/package.json index 5ca07f2..47284f4 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "@looksrare/contracts-exchange-v1", - "version": "1.1.0", - "description": "LooksRare exchange smart contracts (v1)", - "author": "LooksRare", + "name": "@benddao/exchange", + "version": "1.0.0", + "description": "Bend exchange smart", + "author": "BendDAO", "license": "MIT", "private": false, "files": [ @@ -15,16 +15,16 @@ "/contracts/transferManagers/*.sol" ], "keywords": [ - "looksrare" + "benddao" ], "engines": { "node": ">=8.3.0" }, - "homepage": "https://looksrare.org/", - "bugs": "https://github.com/LooksRare/contracts-exchange-v1/issues", + "homepage": "https://benddao.xyz/", + "bugs": "https://github.com/BendDAO/bend-exchange-protocol/issues", "repository": { "type": "git", - "url": "https://github.com/LooksRare/contracts-exchange-v1.git" + "url": "https://github.com/BendDAO/bend-exchange-protocol.git" }, "publishConfig": { "access": "public", @@ -39,7 +39,6 @@ "prepare": "husky install", "test": "hardhat test", "test:gas": "REPORT_GAS=true hardhat test", - "test:forge": "forge test", "test:coverage": "hardhat coverage && hardhat compile --force", "release": "release-it" }, diff --git a/test/AuthorizationManager.test.ts b/test/AuthorizationManager.test.ts new file mode 100644 index 0000000..884432a --- /dev/null +++ b/test/AuthorizationManager.test.ts @@ -0,0 +1,163 @@ +import { expect } from "chai"; +import { defaultAbiCoder, parseEther } from "ethers/lib/utils"; +import { ethers } from "hardhat"; +import { Contracts, Env, makeSuite } from "./_setup"; +import { constants } from "ethers"; + +/* eslint-disable no-unused-expressions */ +makeSuite("AuthorizationManager", (contracts: Contracts, env: Env) => { + it("AuthenticatedProxy - Revertions work as expected", async () => { + const user = env.accounts[1]; + const anotherUser = env.accounts[2]; + const authorizedUser = env.accounts[3]; + + const AuthorizationManager = await ethers.getContractFactory("AuthorizationManager"); + const authorizationManager = await AuthorizationManager.deploy(contracts.weth.address, authorizedUser.address); + await authorizationManager.deployed(); + + await authorizationManager.connect(user).registerProxy(); + const userProxyAddress = await authorizationManager.proxies(user.address); + const userProxyContract = (await ethers.getContractFactory("AuthenticatedProxy")).attach(userProxyAddress); + + const nonTransferERC20 = await (await ethers.getContractFactory("MockNonTransferERC20")).deploy("", ""); + await nonTransferERC20.deployed(); + + await expect( + userProxyContract.connect(user).safeTransfer(nonTransferERC20.address, anotherUser.address, constants.MaxUint256) + ).to.be.revertedWith("Proxy: transfer failed"); + + await expect(userProxyContract.connect(user).withdrawToken(nonTransferERC20.address)).to.be.revertedWith( + "Proxy: withdraw token failed" + ); + + await expect( + user.sendTransaction({ + to: userProxyAddress, + value: parseEther("1.0"), + }) + ).to.be.revertedWith("Receive not allowed"); + + const nonPayable = await (await ethers.getContractFactory("MockNonPayable")).deploy(); + await nonPayable.deployed(); + await nonPayable.registerProxy(authorizationManager.address); + + const nonPayableProxyAddress = await authorizationManager.proxies(nonPayable.address); + const nonPayableProxyContract = (await ethers.getContractFactory("AuthenticatedProxy")).attach( + nonPayableProxyAddress + ); + + await expect(nonPayableProxyContract.connect(authorizedUser).withdrawETH()).to.be.revertedWith( + "Proxy: withdraw ETH failed" + ); + }); + it("AuthorizationManager - Revertions work as expected", async () => { + await expect(contracts.authorizationManager.connect(env.accounts[1]).registerProxy()).to.be.revertedWith( + "Authorization: user already has a proxy" + ); + }); + + it("AuthorizationManager - Owner revertions work as expected", async () => { + const notAdminUser = env.accounts[3]; + + await expect(contracts.authorizationManager.connect(notAdminUser).revoke()).to.be.revertedWith( + "Ownable: caller is not the owner" + ); + }); + it("AuthorizationManager - Owner functions are only callable by owner", async () => { + await expect(contracts.authorizationManager.connect(env.admin).revoke()).to.emit( + contracts.authorizationManager, + "Revoked" + ); + }); + + it("AuthenticatedProxy - access control", async () => { + const user = env.accounts[1]; + const anotherUser = env.accounts[2]; + + const proxyAddress = await contracts.authorizationManager.proxies(user.address); + const proxyContract = (await ethers.getContractFactory("AuthenticatedProxy")).attach(proxyAddress); + + await expect(proxyContract.connect(anotherUser).setRevoke(true)).to.be.revertedWith("Proxy: permission denied"); + await expect( + proxyContract.connect(anotherUser).safeTransfer(contracts.weth.address, anotherUser.address, constants.MaxUint256) + ).to.be.revertedWith("Proxy: permission denied"); + await expect(proxyContract.connect(anotherUser).withdrawETH()).to.be.revertedWith("Proxy: permission denied"); + await expect(proxyContract.connect(anotherUser).withdrawToken(contracts.weth.address)).to.be.revertedWith( + "Proxy: permission denied" + ); + await expect( + proxyContract.connect(anotherUser).delegatecall(contracts.transferERC721.address, defaultAbiCoder.encode([], [])) + ).to.be.revertedWith("Proxy: permission denied"); + + await expect(proxyContract.connect(user).setRevoke(true)).to.emit(proxyContract, "Revoked").withArgs(true); + + expect(proxyContract.connect(user).withdrawETH()).to.be.ok; + expect(proxyContract.connect(user).withdrawToken(contracts.weth.address)).to.be.ok; + expect( + proxyContract + .connect(user) + .delegatecall( + contracts.transferERC721.address, + defaultAbiCoder.encode( + ["address", "address", "address", "uint256", "uint256"], + [contracts.mockERC721.address, user.address, anotherUser.address, constants.Zero, constants.Zero] + ) + ) + ).to.be.ok; + }); + + it("AuthenticatedProxy - authorized Address", async () => { + const user = env.accounts[1]; + const anotherUser = env.accounts[2]; + const authorizedUser = env.accounts[3]; + + const AuthorizationManager = await ethers.getContractFactory("AuthorizationManager"); + const authorizationManager = await AuthorizationManager.deploy(contracts.weth.address, authorizedUser.address); + await authorizationManager.deployed(); + + await authorizationManager.connect(user).registerProxy(); + + const proxyAddress = await authorizationManager.proxies(user.address); + + const proxyContract = (await ethers.getContractFactory("AuthenticatedProxy")).attach(proxyAddress); + + expect(proxyContract.connect(authorizedUser).withdrawETH()).to.be.ok; + expect(proxyContract.connect(authorizedUser).withdrawToken(contracts.weth.address)).to.be.ok; + expect( + proxyContract + .connect(authorizedUser) + .delegatecall( + contracts.transferERC721.address, + defaultAbiCoder.encode( + ["address", "address", "address", "uint256", "uint256"], + [contracts.mockERC721.address, user.address, anotherUser.address, constants.Zero, constants.Zero] + ) + ) + ).to.be.ok; + + await proxyContract.connect(user).setRevoke(true); + + await expect(proxyContract.connect(authorizedUser).withdrawETH()).to.be.revertedWith("Proxy: permission denied"); + await expect(proxyContract.connect(authorizedUser).withdrawToken(contracts.weth.address)).to.be.revertedWith( + "Proxy: permission denied" + ); + await expect( + proxyContract + .connect(authorizedUser) + .delegatecall(contracts.transferERC721.address, defaultAbiCoder.encode([], [])) + ).to.be.revertedWith("Proxy: permission denied"); + + await proxyContract.connect(user).setRevoke(false); + await authorizationManager.revoke(); + + await expect(proxyContract.connect(authorizedUser).withdrawETH()).to.be.revertedWith("Proxy: permission denied"); + await expect(proxyContract.connect(authorizedUser).withdrawToken(contracts.weth.address)).to.be.revertedWith( + "Proxy: permission denied" + ); + await expect( + proxyContract + .connect(authorizedUser) + .delegatecall(contracts.transferERC721.address, defaultAbiCoder.encode([], [])) + ).to.be.revertedWith("Proxy: permission denied"); + }); +}); diff --git a/test/BendExchange.test.ts b/test/BendExchange.test.ts new file mode 100644 index 0000000..b61db6a --- /dev/null +++ b/test/BendExchange.test.ts @@ -0,0 +1,2919 @@ +import { assert, expect } from "chai"; +import { BigNumber, constants, utils } from "ethers"; +import { ethers } from "hardhat"; + +import { increaseTo } from "./helpers/block-traveller"; +import { MakerOrderWithSignature, TakerOrder } from "./helpers/order-types"; +import { createMakerOrder, createTakerOrder } from "./helpers/order-helper"; +import { computeOrderHash } from "./helpers/signature-helper"; +import { Contracts, Env, makeSuite, Snapshots } from "./_setup"; +import { gasCost } from "./helpers/gas-helper"; +const { defaultAbiCoder, parseEther } = utils; + +makeSuite("BendDAO Exchange", (contracts: Contracts, env: Env, snapshots: Snapshots) => { + let startTimeOrder: BigNumber; + let endTimeOrder: BigNumber; + const emptyEncodedBytes = defaultAbiCoder.encode([], []); + + beforeEach(async () => { + // Set up defaults startTime/endTime (for orders) + startTimeOrder = BigNumber.from((await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp); + endTimeOrder = startTimeOrder.add(BigNumber.from("1000")); + }); + + afterEach(async () => { + await snapshots.revert("setup"); + }); + + describe("#1 - Regular sales", async () => { + it("Standard Order ERC721/ETH - MakerAsk order is matched by TakerBid order, maker require ETH", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: constants.AddressZero, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + price: parseEther("3"), + tokenId: constants.Zero, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + const expectedFeeBalanceInWETH = (await contracts.weth.balanceOf(env.feeRecipient.address)).add( + parseEther("3").mul(env.standardProtocolFee).div(10000) + ); + const expectedTakerBalanceInETH = (await ethers.provider.getBalance(takerBidUser.address)).sub(parseEther("3")); + const expectedMakerBalanceInETH = (await ethers.provider.getBalance(makerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH); + + const tx = await contracts.bendExchange + .connect(takerBidUser) + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + makerAskOrder.tokenId, + makerAskOrder.amount, + takerBidOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), takerBidUser.address); + + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + assert.deepEqual(expectedMakerBalanceInETH, await ethers.provider.getBalance(makerAskUser.address)); + assert.deepEqual( + expectedTakerBalanceInETH.sub(await gasCost(tx)), + await ethers.provider.getBalance(takerBidUser.address) + ); + + // Orders that have been executed cannot be matched again + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Order: matching order expired"); + }); + + it("Standard Order ERC721/ETH - MakerAsk order is matched by TakerBid order, maker require WETH", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + price: parseEther("3"), + tokenId: constants.Zero, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const expectedFeeBalanceInWETH = (await contracts.weth.balanceOf(env.feeRecipient.address)).add( + parseEther("3").mul(env.standardProtocolFee).div(10000) + ); + const expectedMakerBalanceInWETH = (await contracts.weth.balanceOf(makerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH); + const expectedTakerBalanceInETH = (await ethers.provider.getBalance(takerBidUser.address)).sub(parseEther("3")); + + const tx = await contracts.bendExchange + .connect(takerBidUser) + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + makerAskOrder.tokenId, + makerAskOrder.amount, + takerBidOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), takerBidUser.address); + + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + assert.deepEqual(expectedMakerBalanceInWETH, await contracts.weth.balanceOf(makerAskUser.address)); + assert.deepEqual( + expectedTakerBalanceInETH.sub(await gasCost(tx)), + await ethers.provider.getBalance(takerBidUser.address) + ); + + // Orders that have been executed cannot be matched again + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Order: matching order expired"); + }); + + it("Standard Order ERC721/(ETH + WETH) - MakerAsk order is matched by TakerBid order, maker require ETH", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: constants.AddressZero, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: constants.Zero, + price: parseEther("3"), + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + // Order is worth 3 ETH; taker user splits it as 2 ETH + 1 WETH + const expectedTakerBalanceInETH = (await ethers.provider.getBalance(takerBidUser.address)).sub(parseEther("2")); + const expectedTakerBalanceInWETH = (await contracts.weth.balanceOf(takerBidUser.address)).sub(parseEther("1")); + const expectedFeeBalanceInWETH = (await contracts.weth.balanceOf(env.feeRecipient.address)).add( + parseEther("3").mul(env.standardProtocolFee).div(10000) + ); + const expectedMakerBalanceInETH = (await ethers.provider.getBalance(makerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH); + + const tx = await contracts.bendExchange + .connect(takerBidUser) + + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: parseEther("2"), + }); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + makerAskOrder.tokenId, + makerAskOrder.amount, + takerBidOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), takerBidUser.address); + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + + // Check balance of WETH is same as expected + assert.deepEqual(expectedTakerBalanceInWETH, await contracts.weth.balanceOf(takerBidUser.address)); + assert.deepEqual( + expectedTakerBalanceInETH.sub(await gasCost(tx)), + await ethers.provider.getBalance(takerBidUser.address) + ); + assert.deepEqual(expectedMakerBalanceInETH, await ethers.provider.getBalance(makerAskUser.address)); + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + }); + + it("Standard Order ERC721/(ETH + WETH) - MakerAsk order is matched by TakerBid order, maker require WETH", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: constants.Zero, + price: parseEther("3"), + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + // Order is worth 3 ETH; taker user splits it as 2 ETH + 1 WETH + const expectedTakerBalanceInETH = (await ethers.provider.getBalance(takerBidUser.address)).sub(parseEther("2")); + const expectedTakerBalanceInWETH = (await contracts.weth.balanceOf(takerBidUser.address)).sub(parseEther("1")); + + const expectedFeeBalanceInWETH = (await contracts.weth.balanceOf(env.feeRecipient.address)).add( + parseEther("3").mul(env.standardProtocolFee).div(10000) + ); + + const expectedMakerBalanceInWETH = (await contracts.weth.balanceOf(makerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH); + + const tx = await contracts.bendExchange + .connect(takerBidUser) + + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: parseEther("2"), + }); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + makerAskOrder.tokenId, + makerAskOrder.amount, + takerBidOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), takerBidUser.address); + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + + // Check balance of WETH is same as expected + assert.deepEqual(expectedTakerBalanceInWETH, await contracts.weth.balanceOf(takerBidUser.address)); + assert.deepEqual( + expectedTakerBalanceInETH.sub(await gasCost(tx)), + await ethers.provider.getBalance(takerBidUser.address) + ); + assert.deepEqual(expectedMakerBalanceInWETH, await contracts.weth.balanceOf(makerAskUser.address)); + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + }); + + it("Standard Order ERC1155/ETH - MakerAsk order is matched by TakerBid order, maker require ETH", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC1155.address, + tokenId: constants.One, + price: parseEther("3"), + amount: constants.Two, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: constants.AddressZero, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: constants.One, + price: parseEther("3"), + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const expectedTakerBalanceInETH = (await ethers.provider.getBalance(takerBidUser.address)).sub(parseEther("3")); + + const expectedFeeBalanceInWETH = (await contracts.weth.balanceOf(env.feeRecipient.address)).add( + parseEther("3").mul(env.standardProtocolFee).div(10000) + ); + + const expectedMakerBalanceInETH = (await ethers.provider.getBalance(makerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH); + + const tx = await contracts.bendExchange + .connect(takerBidUser) + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + makerAskOrder.tokenId, + makerAskOrder.amount, + takerBidOrder.price + ); + + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + + assert.deepEqual( + expectedTakerBalanceInETH.sub(await gasCost(tx)), + await ethers.provider.getBalance(takerBidUser.address) + ); + assert.deepEqual(expectedMakerBalanceInETH, await ethers.provider.getBalance(makerAskUser.address)); + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + + // User 2 had minted 2 tokenId=1 so he has 4 + assert.equal((await contracts.mockERC1155.balanceOf(takerBidUser.address, "1")).toString(), "4"); + }); + + it("Standard Order ERC1155/ETH - MakerAsk order is matched by TakerBid order, maker require WETH", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC1155.address, + tokenId: constants.One, + price: parseEther("3"), + amount: constants.Two, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: constants.One, + price: parseEther("3"), + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const expectedTakerBalanceInETH = (await ethers.provider.getBalance(takerBidUser.address)).sub(parseEther("3")); + + const expectedFeeBalanceInWETH = (await contracts.weth.balanceOf(env.feeRecipient.address)).add( + parseEther("3").mul(env.standardProtocolFee).div(10000) + ); + + const expectedMakerBalanceInWETH = (await contracts.weth.balanceOf(makerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH); + + const tx = await contracts.bendExchange + .connect(takerBidUser) + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + makerAskOrder.tokenId, + makerAskOrder.amount, + takerBidOrder.price + ); + + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + + assert.deepEqual( + expectedTakerBalanceInETH.sub(await gasCost(tx)), + await ethers.provider.getBalance(takerBidUser.address) + ); + assert.deepEqual(expectedMakerBalanceInWETH, await contracts.weth.balanceOf(makerAskUser.address)); + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + // User 2 had minted 2 tokenId=1 so he has 4 + assert.equal((await contracts.mockERC1155.balanceOf(takerBidUser.address, "1")).toString(), "4"); + }); + + it("Standard Order ERC721/WETH - MakerBid order is matched by TakerAsk order, maker bid with ETH", async () => { + const makerBidUser = env.accounts[2]; + const takerAskUser = env.accounts[1]; + + const makerBidOrder = await createMakerOrder({ + isOrderAsk: false, + maker: makerBidUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: constants.AddressZero, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerBidUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerAskOrder = createTakerOrder({ + isOrderAsk: true, + taker: takerAskUser.address, + tokenId: constants.Zero, + price: makerBidOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const expectedFeeBalanceInWETH = (await contracts.weth.balanceOf(env.feeRecipient.address)).add( + parseEther("3").mul(env.standardProtocolFee).div(10000) + ); + + const expectedMakerBalanceInWETH = (await contracts.weth.balanceOf(makerBidUser.address)).sub(parseEther("3")); + + const expectedTakerBalanceInETH = (await ethers.provider.getBalance(takerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH); + + const tx = await contracts.bendExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerAsk") + .withArgs( + computeOrderHash(makerBidOrder), + makerBidOrder.nonce, + takerAskUser.address, + makerBidUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerBidOrder.currency, + makerBidOrder.collection, + takerAskOrder.tokenId, + makerBidOrder.amount, + makerBidOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), makerBidUser.address); + + assert.deepEqual(expectedMakerBalanceInWETH, await contracts.weth.balanceOf(makerBidUser.address)); + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + assert.deepEqual( + expectedTakerBalanceInETH.sub(await gasCost(tx)), + await ethers.provider.getBalance(takerAskUser.address) + ); + + expect(expectedTakerBalanceInETH).to.be.gte(await ethers.provider.getBalance(takerAskUser.address)); + + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerBidUser.address, makerBidOrder.nonce) + ); + }); + + it("Standard Order ERC721/WETH - MakerBid order is matched by TakerAsk order, maker bid with WETH", async () => { + const makerBidUser = env.accounts[2]; + const takerAskUser = env.accounts[1]; + + const makerBidOrder = await createMakerOrder({ + isOrderAsk: false, + maker: makerBidUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerBidUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerAskOrder = createTakerOrder({ + isOrderAsk: true, + taker: takerAskUser.address, + tokenId: constants.Zero, + price: makerBidOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const expectedFeeBalanceInWETH = (await contracts.weth.balanceOf(env.feeRecipient.address)).add( + parseEther("3").mul(env.standardProtocolFee).div(10000) + ); + + const expectedMakerBalanceInWETH = (await contracts.weth.balanceOf(makerBidUser.address)).sub(parseEther("3")); + + const expectedTakerBalanceInWETH = (await contracts.weth.balanceOf(takerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH); + + const tx = await contracts.bendExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerAsk") + .withArgs( + computeOrderHash(makerBidOrder), + makerBidOrder.nonce, + takerAskUser.address, + makerBidUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerBidOrder.currency, + makerBidOrder.collection, + takerAskOrder.tokenId, + makerBidOrder.amount, + makerBidOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), makerBidUser.address); + + assert.deepEqual(expectedMakerBalanceInWETH, await contracts.weth.balanceOf(makerBidUser.address)); + assert.deepEqual(expectedTakerBalanceInWETH, await contracts.weth.balanceOf(takerAskUser.address)); + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerBidUser.address, makerBidOrder.nonce) + ); + }); + + it("Standard Order ERC1155/WETH - MakerBid order is matched by TakerAsk order, maker bid with ETH", async () => { + const makerBidUser = env.accounts[1]; + const takerAskUser = env.accounts[2]; + + const makerBidOrder = await createMakerOrder({ + isOrderAsk: false, + maker: makerBidUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC1155.address, + tokenId: BigNumber.from("3"), + price: parseEther("3"), + amount: constants.Two, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: constants.AddressZero, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerBidUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerAskOrder = createTakerOrder({ + isOrderAsk: true, + taker: takerAskUser.address, + tokenId: BigNumber.from("3"), + price: makerBidOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const expectedFeeBalanceInWETH = (await contracts.weth.balanceOf(env.feeRecipient.address)).add( + parseEther("3").mul(env.standardProtocolFee).div(10000) + ); + + const expectedMakerBalanceInWETH = (await contracts.weth.balanceOf(makerBidUser.address)).sub(parseEther("3")); + + const expectedTakerBalanceInETH = (await ethers.provider.getBalance(takerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH); + + const tx = await contracts.bendExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerAsk") + .withArgs( + computeOrderHash(makerBidOrder), + makerBidOrder.nonce, + takerAskUser.address, + makerBidUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerBidOrder.currency, + makerBidOrder.collection, + takerAskOrder.tokenId, + makerBidOrder.amount, + makerBidOrder.price + ); + + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerBidUser.address, makerBidOrder.nonce) + ); + + assert.deepEqual(expectedMakerBalanceInWETH, await contracts.weth.balanceOf(makerBidUser.address)); + assert.deepEqual( + expectedTakerBalanceInETH.sub(await gasCost(tx)), + await ethers.provider.getBalance(takerAskUser.address) + ); + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + }); + + it("Standard Order ERC1155/WETH - MakerBid order is matched by TakerAsk order, maker bid with WETH", async () => { + const makerBidUser = env.accounts[1]; + const takerAskUser = env.accounts[2]; + + const makerBidOrder = await createMakerOrder({ + isOrderAsk: false, + maker: makerBidUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC1155.address, + tokenId: BigNumber.from("3"), + price: parseEther("3"), + amount: constants.Two, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerBidUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerAskOrder = createTakerOrder({ + isOrderAsk: true, + taker: takerAskUser.address, + tokenId: BigNumber.from("3"), + price: makerBidOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const expectedFeeBalanceInWETH = (await contracts.weth.balanceOf(env.feeRecipient.address)).add( + parseEther("3").mul(env.standardProtocolFee).div(10000) + ); + + const expectedMakerBalanceInWETH = (await contracts.weth.balanceOf(makerBidUser.address)).sub(parseEther("3")); + + const expectedTakerBalanceInWETH = (await contracts.weth.balanceOf(takerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH); + + const tx = await contracts.bendExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerAsk") + .withArgs( + computeOrderHash(makerBidOrder), + makerBidOrder.nonce, + takerAskUser.address, + makerBidUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerBidOrder.currency, + makerBidOrder.collection, + takerAskOrder.tokenId, + makerBidOrder.amount, + makerBidOrder.price + ); + + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerBidUser.address, makerBidOrder.nonce) + ); + + assert.deepEqual(expectedMakerBalanceInWETH, await contracts.weth.balanceOf(makerBidUser.address)); + assert.deepEqual(expectedTakerBalanceInWETH, await contracts.weth.balanceOf(takerAskUser.address)); + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + }); + }); + + describe("#2 - Non-standard orders", async () => { + it("ERC1271/Contract Signature - MakerBid order is matched by TakerAsk order", async () => { + const userSigningThroughContract = env.accounts[1]; + const takerAskUser = env.accounts[2]; + + const MockSignerContract = await ethers.getContractFactory("MockSignerContract"); + const mockSignerContract = await MockSignerContract.connect(userSigningThroughContract).deploy(); + await mockSignerContract.deployed(); + + await contracts.weth.connect(userSigningThroughContract).transfer(mockSignerContract.address, parseEther("1")); + + await mockSignerContract + .connect(userSigningThroughContract) + .registerProxy(contracts.authorizationManager.address); + + await mockSignerContract + .connect(userSigningThroughContract) + .approveERC20ToBeSpent( + contracts.weth.address, + await contracts.authorizationManager.proxies(mockSignerContract.address) + ); + + const makerBidOrder = await createMakerOrder({ + isOrderAsk: false, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + maker: mockSignerContract.address, + collection: contracts.mockERC721.address, + tokenId: constants.One, + price: parseEther("1"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: userSigningThroughContract, + verifyingContract: contracts.bendExchange.address, + }); + + const takerAskOrder = createTakerOrder({ + isOrderAsk: true, + taker: takerAskUser.address, + tokenId: makerBidOrder.tokenId, + price: makerBidOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const tx = await contracts.bendExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerAsk") + .withArgs( + computeOrderHash(makerBidOrder), + makerBidOrder.nonce, + takerAskUser.address, + mockSignerContract.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerBidOrder.currency, + makerBidOrder.collection, + takerAskOrder.tokenId, + makerBidOrder.amount, + makerBidOrder.price + ); + + // Verify funds/tokens were transferred + assert.equal(await contracts.mockERC721.ownerOf("1"), mockSignerContract.address); + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled( + mockSignerContract.address, + makerBidOrder.nonce + ) + ); + + // Withdraw it back + await mockSignerContract.connect(userSigningThroughContract).withdrawERC721NFT(contracts.mockERC721.address, "1"); + assert.equal(await contracts.mockERC721.ownerOf("1"), userSigningThroughContract.address); + }); + + it("ERC1271/Contract Signature - MakerAsk order is matched by TakerBid order", async () => { + const userSigningThroughContract = env.accounts[1]; + const takerBidUser = env.accounts[2]; + const MockSignerContract = await ethers.getContractFactory("MockSignerContract"); + const mockSignerContract = await MockSignerContract.connect(userSigningThroughContract).deploy(); + await mockSignerContract.deployed(); + + await contracts.mockERC721 + .connect(userSigningThroughContract) + .transferFrom(userSigningThroughContract.address, mockSignerContract.address, "0"); + + await mockSignerContract + .connect(userSigningThroughContract) + .registerProxy(contracts.authorizationManager.address); + + await mockSignerContract + .connect(userSigningThroughContract) + .approveERC721NFT( + contracts.mockERC721.address, + await contracts.authorizationManager.proxies(mockSignerContract.address) + ); + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: mockSignerContract.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("1"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: userSigningThroughContract, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const tx = await contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + mockSignerContract.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + takerBidOrder.tokenId, + makerAskOrder.amount, + makerAskOrder.price + ); + + // Verify funds/tokens were transferred + assert.equal(await contracts.mockERC721.ownerOf("1"), takerBidUser.address); + assert.deepEqual( + await contracts.weth.balanceOf(mockSignerContract.address), + takerBidOrder.price.mul("9800").div("10000") + ); + + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled( + mockSignerContract.address, + makerAskOrder.nonce + ) + ); + + // Withdraw WETH back + await mockSignerContract.connect(userSigningThroughContract).withdrawERC20(contracts.weth.address); + assert.deepEqual(await contracts.weth.balanceOf(mockSignerContract.address), constants.Zero); + }); + }); + + describe("#3 - Royalty fee system", async () => { + it("Fee/Royalty - Payment with ERC2981 works for non-ETH orders", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + assert.equal(await contracts.mockERC721WithRoyalty.RECEIVER(), env.royaltyCollector.address); + assert.isTrue(await contracts.mockERC721WithRoyalty.supportsInterface("0x2a55205a")); + + // Verify balance of env.royaltyCollector is 0 + assert.deepEqual(await contracts.weth.balanceOf(env.royaltyCollector.address), constants.Zero); + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721WithRoyalty.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + price: makerAskOrder.price, + tokenId: makerAskOrder.tokenId, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + const tx = await contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + takerBidOrder.tokenId, + makerAskOrder.amount, + makerAskOrder.price + ); + + const expectedRoyaltyAmount = BigNumber.from(takerBidOrder.price).mul("200").div("10000"); + + await expect(tx) + .to.emit(contracts.bendExchange, "RoyaltyPayment") + .withArgs( + makerAskOrder.collection, + takerBidOrder.tokenId, + env.royaltyCollector.address, + makerAskOrder.currency, + expectedRoyaltyAmount + ); + + assert.equal(await contracts.mockERC721WithRoyalty.ownerOf("0"), takerBidUser.address); + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + + // Verify WETH balance of royalty collector has increased + assert.deepEqual(await contracts.weth.balanceOf(env.royaltyCollector.address), expectedRoyaltyAmount); + }); + + it("Fee/Royalty - Payment with ERC2981 works for ETH orders", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + assert.equal(await contracts.mockERC721WithRoyalty.RECEIVER(), env.royaltyCollector.address); + assert.isTrue(await contracts.mockERC721WithRoyalty.supportsInterface("0x2a55205a")); + + // Verify balance of env.royaltyCollector is 0 + assert.deepEqual(await contracts.weth.balanceOf(env.royaltyCollector.address), constants.Zero); + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721WithRoyalty.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + price: makerAskOrder.price, + tokenId: makerAskOrder.tokenId, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + const tx = await contracts.bendExchange + .connect(takerBidUser) + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: parseEther("3"), + }); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + takerBidOrder.tokenId, + makerAskOrder.amount, + makerAskOrder.price + ); + + const expectedRoyaltyAmount = BigNumber.from(takerBidOrder.price).mul("200").div("10000"); + + await expect(tx) + .to.emit(contracts.bendExchange, "RoyaltyPayment") + .withArgs( + makerAskOrder.collection, + takerBidOrder.tokenId, + env.royaltyCollector.address, + makerAskOrder.currency, + expectedRoyaltyAmount + ); + assert.equal(await contracts.mockERC721WithRoyalty.ownerOf("0"), takerBidUser.address); + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + + // Verify WETH balance of royalty collector has increased + assert.deepEqual(await contracts.weth.balanceOf(env.royaltyCollector.address), expectedRoyaltyAmount); + }); + + it("Fee/Royalty - Payment for custom integration works", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + // Set 3% for royalties + const fee = "300"; + let tx = await contracts.royaltyFeeSetter + .connect(env.admin) + .updateRoyaltyInfoForCollection( + contracts.mockERC721.address, + env.admin.address, + env.royaltyCollector.address, + fee + ); + + await expect(tx) + .to.emit(contracts.royaltyFeeRegistry, "RoyaltyFeeUpdate") + .withArgs(contracts.mockERC721.address, env.admin.address, env.royaltyCollector.address, fee); + + const result = await contracts.royaltyFeeRegistry.royaltyFeeInfoCollection(contracts.mockERC721.address); + assert.equal(result[0], env.admin.address); + assert.equal(result[1], env.royaltyCollector.address); + assert.equal(result[2].toString(), fee); + + // Verify balance of env.royaltyCollector is 0 + assert.deepEqual(await contracts.weth.balanceOf(env.royaltyCollector.address), constants.Zero); + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: constants.Zero, + price: parseEther("3"), + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + tx = await contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + takerBidOrder.tokenId, + makerAskOrder.amount, + makerAskOrder.price + ); + + const expectedRoyaltyAmount = BigNumber.from(takerBidOrder.price).mul(fee).div("10000"); + + await expect(tx) + .to.emit(contracts.bendExchange, "RoyaltyPayment") + .withArgs( + makerAskOrder.collection, + takerBidOrder.tokenId, + env.royaltyCollector.address, + makerAskOrder.currency, + expectedRoyaltyAmount + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), takerBidUser.address); + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + + // Verify WETH balance of royalty collector has increased + assert.deepEqual(await contracts.weth.balanceOf(env.royaltyCollector.address), expectedRoyaltyAmount); + }); + + it("Fee/Royalty - Slippage protection works for MakerAsk", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + // Set 3% for royalties + const fee = "300"; + await contracts.royaltyFeeSetter + .connect(env.admin) + .updateRoyaltyInfoForCollection( + contracts.mockERC721.address, + env.admin.address, + env.royaltyCollector.address, + fee + ); + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: BigNumber.from("9500"), // ProtocolFee: 2%, RoyaltyFee: 3% + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + // Update to 3.01% for royalties + await contracts.royaltyFeeSetter + .connect(env.admin) + .updateRoyaltyInfoForCollection( + contracts.mockERC721.address, + env.admin.address, + env.royaltyCollector.address, + "301" + ); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: constants.Zero, + price: parseEther("3"), + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: parseEther("3"), + }) + ).to.be.revertedWith("Fees: higher than expected"); + + // Update back to 3.00% for royalties + await contracts.royaltyFeeSetter + .connect(env.admin) + .updateRoyaltyInfoForCollection( + contracts.mockERC721.address, + env.admin.address, + env.royaltyCollector.address, + fee + ); + + // Trade is executed + const tx = await contracts.bendExchange + .connect(takerBidUser) + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: parseEther("3"), + }); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + takerBidOrder.tokenId, + makerAskOrder.amount, + makerAskOrder.price + ); + }); + + it("Fee/Royalty - Slippage protection works for TakerAsk", async () => { + const makerBidUser = env.accounts[2]; + const takerAskUser = env.accounts[1]; + + const makerBidOrder = await createMakerOrder({ + isOrderAsk: false, + maker: makerBidUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerBidUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerAskOrder = createTakerOrder({ + isOrderAsk: true, + taker: takerAskUser.address, + tokenId: constants.Zero, + price: parseEther("3"), + minPercentageToAsk: BigNumber.from("9500"), // ProtocolFee: 2%, RoyaltyFee: 3% + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + // Update to 3.01% for royalties + await contracts.royaltyFeeSetter + .connect(env.admin) + .updateRoyaltyInfoForCollection( + contracts.mockERC721.address, + env.admin.address, + env.royaltyCollector.address, + "301" + ); + + await expect( + contracts.bendExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder) + ).to.be.revertedWith("Fees: higher than expected"); + + // Update back to 3.00% for royalties + await contracts.royaltyFeeSetter + .connect(env.admin) + .updateRoyaltyInfoForCollection( + contracts.mockERC721.address, + env.admin.address, + env.royaltyCollector.address, + "300" + ); + + const tx = await contracts.bendExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerAsk") + .withArgs( + computeOrderHash(makerBidOrder), + makerBidOrder.nonce, + takerAskUser.address, + makerBidUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerBidOrder.currency, + makerBidOrder.collection, + takerAskOrder.tokenId, + makerBidOrder.amount, + makerBidOrder.price + ); + }); + + it("Fee/Royalty/Private Sale - Royalty fee is collected but no platform fee", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + // Verify balance of env.royaltyCollector is 0 + assert.deepEqual(await contracts.weth.balanceOf(env.royaltyCollector.address), constants.Zero); + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721WithRoyalty.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyPrivateSale.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: defaultAbiCoder.encode(["address"], [takerBidUser.address]), // target user + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: constants.Zero, + price: parseEther("3"), + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + const tx = await contracts.bendExchange + .connect(takerBidUser) + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: parseEther("3"), + }); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyPrivateSale.address, + makerAskOrder.currency, + makerAskOrder.collection, + takerBidOrder.tokenId, + makerAskOrder.amount, + makerAskOrder.price + ); + + assert.equal(await contracts.mockERC721WithRoyalty.ownerOf("0"), takerBidUser.address); + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + + // Verify WETH balance of royalty collector has increased + assert.deepEqual( + await contracts.weth.balanceOf(env.royaltyCollector.address), + takerBidOrder.price.mul("200").div("10000") + ); + + // Verify balance of env.admin (aka treasury) is 0 + assert.deepEqual(await contracts.weth.balanceOf(env.admin.address), constants.Zero); + }); + }); + + describe("#4 - Standard logic revertions", async () => { + it("One Cancel Other - Initial order is not executable anymore", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const initialMakerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const adjustedMakerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + price: parseEther("2.5"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + price: parseEther("2.5"), + tokenId: constants.Zero, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const tx = await contracts.bendExchange + .connect(takerBidUser) + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, adjustedMakerAskOrder, { + value: takerBidOrder.price, + }); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(adjustedMakerAskOrder), + adjustedMakerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + adjustedMakerAskOrder.currency, + adjustedMakerAskOrder.collection, + takerBidOrder.tokenId, + adjustedMakerAskOrder.amount, + adjustedMakerAskOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), takerBidUser.address); + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled( + makerAskUser.address, + adjustedMakerAskOrder.nonce + ) + ); + + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled( + makerAskUser.address, + initialMakerAskOrder.nonce + ) + ); + + // Initial order is not executable anymore + await expect( + contracts.bendExchange + .connect(takerBidUser) + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, initialMakerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Order: matching order expired"); + }); + + it("Cancel - Cannot match if order was cancelled", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const tx = await contracts.bendExchange.connect(makerAskUser).cancelMultipleMakerOrders([makerAskOrder.nonce]); + // Event params are not tested because of array issue with BN + await expect(tx).to.emit(contracts.bendExchange, "CancelMultipleOrders"); + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Order: matching order expired"); + }); + + it("Cancel - Cannot match if on a different checkpoint than current on-chain maker's checkpoint", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[3]; + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + maker: makerAskUser.address, + collection: contracts.mockERC721.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const tx = await contracts.bendExchange.connect(makerAskUser).cancelAllOrdersForSender("1"); + await expect(tx).to.emit(contracts.bendExchange, "CancelAllOrders").withArgs(makerAskUser.address, "1"); + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Order: matching order expired"); + }); + + it("Order - Cannot match if msg.value is too high", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[3]; + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + maker: makerAskUser.address, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + price: makerAskOrder.price, + tokenId: makerAskOrder.tokenId, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price.add(constants.One), + }) + ).to.be.revertedWith("Order: msg.value too high"); + }); + + it("Order - Cannot match is amount is 0", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[3]; + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.Zero, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder, {}) + ).to.be.revertedWith("Order: amount cannot be 0"); + }); + + it("Order - Cannot match 2 ask orders, 2 bid orders, or taker not the sender", async () => { + const makerAskUser = env.accounts[2]; + const fakeTakerUser = env.accounts[3]; + const takerBidUser = env.accounts[4]; + + // 1. MATCH ASK WITH TAKER BID + // 1.1 Signer is not the actual maker + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(fakeTakerUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder, {}) + ).to.be.revertedWith("Order: taker must be the sender"); + + await expect( + contracts.bendExchange + .connect(fakeTakerUser) + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Order: taker must be the sender"); + + // 1.2 wrong sides + takerBidOrder.isOrderAsk = true; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder, {}) + ).to.be.revertedWith("Order: wrong sides"); + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Order: wrong sides"); + + makerAskOrder.isOrderAsk = false; + + // No need to duplicate tests again + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder, {}) + ).to.be.revertedWith("Order: wrong sides"); + + takerBidOrder.isOrderAsk = false; + + // No need to duplicate tests again + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) + ).to.be.revertedWith("Order: wrong sides"); + + // 2. MATCH ASK WITH TAKER BID + // 2.1 Signer is not the actual maker + const takerAskUser = env.accounts[1]; + const makerBidUser = env.accounts[2]; + + const makerBidOrder = await createMakerOrder({ + isOrderAsk: false, + maker: makerBidUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerBidUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerAskOrder = createTakerOrder({ + isOrderAsk: true, + taker: takerAskUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + await expect( + contracts.bendExchange.connect(fakeTakerUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder) + ).to.be.revertedWith("Order: taker must be the sender"); + + // 2.2 wrong sides + takerAskOrder.isOrderAsk = false; + + await expect( + contracts.bendExchange.connect(makerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder) + ).to.be.revertedWith("Order: wrong sides"); + + makerBidOrder.isOrderAsk = true; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder) + ).to.be.revertedWith("Order: wrong sides"); + + takerAskOrder.isOrderAsk = true; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder, {}) + ).to.be.revertedWith("Order: wrong sides"); + }); + + it("Cancel - Cannot cancel all at an nonce equal or lower than existing one", async () => { + await expect(contracts.bendExchange.connect(env.accounts[1]).cancelAllOrdersForSender("0")).to.be.revertedWith( + "Cancel: order nonce lower than current" + ); + + await expect( + contracts.bendExchange.connect(env.accounts[1]).cancelAllOrdersForSender("500000") + ).to.be.revertedWith("Cancel: can not cancel more orders"); + + // Change the minimum nonce for user to 2 + await contracts.bendExchange.connect(env.accounts[1]).cancelAllOrdersForSender("2"); + + await expect(contracts.bendExchange.connect(env.accounts[1]).cancelAllOrdersForSender("1")).to.be.revertedWith( + "Cancel: order nonce lower than current" + ); + + await expect(contracts.bendExchange.connect(env.accounts[1]).cancelAllOrdersForSender("2")).to.be.revertedWith( + "Cancel: order nonce lower than current" + ); + }); + + it("Cancel - Cannot cancel all at an nonce equal than existing one", async () => { + // Change the minimum nonce for user to 2 + await contracts.bendExchange.connect(env.accounts[1]).cancelAllOrdersForSender("2"); + + await expect(contracts.bendExchange.connect(env.accounts[1]).cancelMultipleMakerOrders(["0"])).to.be.revertedWith( + "Cancel: order nonce lower than current" + ); + + await expect( + contracts.bendExchange.connect(env.accounts[1]).cancelMultipleMakerOrders(["3", "1"]) + ).to.be.revertedWith("Cancel: order nonce lower than current"); + + // Can cancel at the same nonce that minimum one + await contracts.bendExchange.connect(env.accounts[1]).cancelMultipleMakerOrders(["2"]); + }); + + it("Order - Cannot trade before startTime", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + startTimeOrder = BigNumber.from( + (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + ).add("5000"); + endTimeOrder = startTimeOrder.add(BigNumber.from("10000")); + + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder) + ).to.be.revertedWith("Strategy: execution invalid"); + + await increaseTo(startTimeOrder); + await contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); + }); + + it("Order - Cannot trade after endTime", async () => { + const makerBidUser = env.accounts[2]; + const takerAskUser = env.accounts[1]; + + endTimeOrder = startTimeOrder.add(BigNumber.from("5000")); + + const makerBidOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: false, + maker: makerBidUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerBidUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerAskOrder = createTakerOrder({ + isOrderAsk: true, + taker: takerAskUser.address, + tokenId: makerBidOrder.tokenId, + price: makerBidOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + await increaseTo(endTimeOrder.add(1)); + + await expect( + contracts.bendExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder) + ).to.be.revertedWith("Strategy: execution invalid"); + }); + + it("Currency - Cannot match if currency is removed", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + const tx = await contracts.currencyManager.connect(env.admin).removeCurrency(contracts.weth.address); + await expect(tx).to.emit(contracts.currencyManager, "CurrencyRemoved").withArgs(contracts.weth.address); + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Currency: not whitelisted"); + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder, {}) + ).to.be.revertedWith("Currency: not whitelisted"); + }); + + it("Currency - Cannot use function to match MakerAsk with native asset if maker currency not WETH", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.mockUSDT.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: makerAskOrder.price, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Order: currency must be WETH"); + }); + + it("Currency - Cannot match until currency is whitelisted", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + // Each users mints 1M USDT + await contracts.mockUSDT.connect(takerBidUser).mint(takerBidUser.address, parseEther("1000000")); + + // Set approval for USDT + await contracts.mockUSDT + .connect(takerBidUser) + .approve(await contracts.authorizationManager.proxies(takerBidUser.address), constants.MaxUint256); + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.mockUSDT.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder, {}) + ).to.be.revertedWith("Currency: not whitelisted"); + + let tx = await contracts.currencyManager.connect(env.admin).addCurrency(contracts.mockUSDT.address); + await expect(tx).to.emit(contracts.currencyManager, "CurrencyWhitelisted").withArgs(contracts.mockUSDT.address); + tx = await contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + takerBidOrder.tokenId, + makerAskOrder.amount, + makerAskOrder.price + ); + }); + + it("Strategy - Cannot match if strategy not whitelisted", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + let tx = await contracts.executionManager + .connect(env.admin) + .removeStrategy(contracts.strategyStandardSaleForFixedPrice.address); + await expect(tx) + .to.emit(contracts.executionManager, "StrategyRemoved") + .withArgs(contracts.strategyStandardSaleForFixedPrice.address); + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) + ).to.be.revertedWith("Strategy: not whitelisted"); + + tx = await contracts.executionManager + .connect(env.admin) + .addStrategy(contracts.strategyStandardSaleForFixedPrice.address); + await expect(tx) + .to.emit(contracts.executionManager, "StrategyWhitelisted") + .withArgs(contracts.strategyStandardSaleForFixedPrice.address); + + tx = await contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + takerBidOrder.tokenId, + makerAskOrder.amount, + makerAskOrder.price + ); + }); + + it("Transfer - Cannot match if no transfer", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const MockNonCompliantERC721 = await ethers.getContractFactory("MockNonCompliantERC721"); + const mockNonCompliantERC721 = await MockNonCompliantERC721.deploy("Mock Bad ERC721", "MBERC721"); + await mockNonCompliantERC721.deployed(); + + // User1 mints tokenId=0 + await mockNonCompliantERC721.connect(makerAskUser).mint(makerAskUser.address); + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: mockNonCompliantERC721.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder, {}) + ).to.be.revertedWith("Transfer: no NFT transfer available"); + + let tx = await contracts.transferManager + .connect(env.admin) + .addCollectionTransfer(mockNonCompliantERC721.address, contracts.transferNonCompliantERC721.address); + + await expect(tx) + .to.emit(contracts.transferManager, "CollectionTransferAdded") + .withArgs(mockNonCompliantERC721.address, contracts.transferNonCompliantERC721.address); + + assert.equal( + await contracts.transferManager.transfers(mockNonCompliantERC721.address), + contracts.transferNonCompliantERC721.address + ); + + // User approves custom transfer manager contract + const userProxy = await contracts.authorizationManager.proxies(makerAskUser.address); + await mockNonCompliantERC721.connect(makerAskUser).setApprovalForAll(userProxy, true); + + tx = await contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + takerBidOrder.tokenId, + makerAskOrder.amount, + makerAskOrder.price + ); + + tx = await contracts.transferManager.removeCollectionTransfer(mockNonCompliantERC721.address); + + await expect(tx) + .to.emit(contracts.transferManager, "CollectionTransferRemoved") + .withArgs(mockNonCompliantERC721.address); + + assert.equal(await contracts.transferManager.transfers(mockNonCompliantERC721.address), constants.AddressZero); + }); + + it("Interceptor - Cannot match if interceptor is removed", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + const makerBidUser = takerBidUser; + const takerAskUser = makerAskUser; + + const tx = await contracts.interceptorManager + .connect(env.admin) + .removeCollectionInterceptor(contracts.redeemNFT.address); + await expect(tx) + .to.emit(contracts.interceptorManager, "CollectionInterceptorRemoved") + .withArgs(contracts.redeemNFT.address); + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: contracts.redeemNFT.address, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Interceptor: maker interceptor not whitelisted"); + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder, {}) + ).to.be.revertedWith("Interceptor: maker interceptor not whitelisted"); + + const makerBidOrder = await createMakerOrder({ + isOrderAsk: false, + maker: makerBidUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerBidUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerAskOrder: TakerOrder = { + isOrderAsk: true, + taker: takerAskUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: contracts.redeemNFT.address, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder, {}) + ).to.be.revertedWith("Interceptor: taker interceptor not whitelisted"); + }); + it("Interceptor - TakerBid cannot match until interceptor is whitelisted", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const MockInterceptor = await ethers.getContractFactory("MockInterceptor"); + const mockInterceptor = await MockInterceptor.deploy(); + await mockInterceptor.deployed(); + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + collection: contracts.mockERC721.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: mockInterceptor.address, + interceptorExtra: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder, {}) + ).to.be.revertedWith("Interceptor: maker interceptor not whitelisted"); + + let tx = await contracts.interceptorManager.connect(env.admin).addCollectionInterceptor(mockInterceptor.address); + + await expect(tx) + .to.emit(contracts.interceptorManager, "CollectionInterceptorWhitelisted") + .withArgs(mockInterceptor.address); + + tx = await contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + takerBidOrder.tokenId, + makerAskOrder.amount, + makerAskOrder.price + ); + }); + it("Interceptor - TakerAsk cannot match until interceptor is whitelisted", async () => { + const makerBidUser = env.accounts[2]; + const takerAskUser = env.accounts[1]; + + const MockInterceptor = await ethers.getContractFactory("MockInterceptor"); + const mockInterceptor = await MockInterceptor.deploy(); + await mockInterceptor.deployed(); + + const makerBidOrder = await createMakerOrder({ + isOrderAsk: false, + maker: makerBidUser.address, + collection: contracts.mockERC721.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + signerUser: makerBidUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerAskOrder: TakerOrder = { + isOrderAsk: true, + taker: takerAskUser.address, + tokenId: makerBidOrder.tokenId, + price: makerBidOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: mockInterceptor.address, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder, {}) + ).to.be.revertedWith("Interceptor: taker interceptor not whitelisted"); + + let tx = await contracts.interceptorManager.connect(env.admin).addCollectionInterceptor(mockInterceptor.address); + + await expect(tx) + .to.emit(contracts.interceptorManager, "CollectionInterceptorWhitelisted") + .withArgs(mockInterceptor.address); + + tx = await contracts.bendExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerAsk") + .withArgs( + computeOrderHash(makerBidOrder), + makerBidOrder.nonce, + takerAskUser.address, + makerBidUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerBidOrder.currency, + makerBidOrder.collection, + takerAskOrder.tokenId, + makerBidOrder.amount, + makerBidOrder.price + ); + }); + + it("Authorization - no delegate proxy 1", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[11]; + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + collection: contracts.mockERC721.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder, {}) + ).to.be.revertedWith("Authorization: no delegate proxy"); + }); + it("Authorization - no delegate proxy 2", async () => { + const makerAskUser = env.accounts[11]; + const takerBidUser = env.accounts[1]; + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + collection: contracts.mockERC721.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder, {}) + ).to.be.revertedWith("Authorization: no delegate proxy"); + }); + }); + + describe("#5 - Unusual logic revertions", async () => { + it("SignatureChecker - Cannot match if v parameters is not 27 or 28", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + makerAskOrder.v = 29; + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) + ).to.be.revertedWith("Signature: invalid v parameter"); + }); + + it("SignatureChecker - Cannot match if invalid s parameter", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + // The s value is picked randomly to make the condition be rejected + makerAskOrder.s = "0x9ca0e65dda4b504989e1db8fc30095f24489ee7226465e9545c32fc7853fe985"; + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + price: makerAskOrder.price, + tokenId: makerAskOrder.tokenId, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) + ).to.be.revertedWith("Signature: invalid s parameter"); + }); + + it("Order - Cannot cancel if no order", async () => { + await expect(contracts.bendExchange.connect(env.accounts[1]).cancelMultipleMakerOrders([])).to.be.revertedWith( + "Cancel: can not be empty" + ); + + await expect(contracts.bendExchange.connect(env.accounts[2]).cancelMultipleMakerOrders([])).to.be.revertedWith( + "Cancel: can not be empty" + ); + }); + + it("Order - Cannot execute if maker is null address", async () => { + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: constants.AddressZero, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: env.accounts[3], + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: env.accounts[2].address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(env.accounts[2]).matchAskWithTakerBid(takerBidOrder, makerAskOrder) + ).to.be.revertedWith("Order: invalid maker"); + }); + + it("Order - Cannot execute if wrong maker", async () => { + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: env.accounts[1].address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: env.accounts[3], + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: env.accounts[2].address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(env.accounts[2]).matchAskWithTakerBid(takerBidOrder, makerAskOrder) + ).to.be.revertedWith("Signature: Invalid"); + }); + }); + + describe("#6 - Owner functions and access rights", async () => { + it("Null address in owner functions", async () => { + await expect( + contracts.bendExchange.connect(env.admin).updateCurrencyManager(constants.AddressZero) + ).to.be.revertedWith("Owner: can not be null address"); + + await expect( + contracts.bendExchange.connect(env.admin).updateExecutionManager(constants.AddressZero) + ).to.be.revertedWith("Owner: can not be null address"); + + await expect( + contracts.bendExchange.connect(env.admin).updateRoyaltyFeeManager(constants.AddressZero) + ).to.be.revertedWith("Owner: can not be null address"); + + await expect( + contracts.bendExchange.connect(env.admin).updateTransferManager(constants.AddressZero) + ).to.be.revertedWith("Owner: can not be null address"); + + await expect( + contracts.bendExchange.connect(env.admin).updateAuthorizationManager(constants.AddressZero) + ).to.be.revertedWith("Owner: can not be null address"); + + await expect( + contracts.bendExchange.connect(env.admin).updateInterceptorManager(constants.AddressZero) + ).to.be.revertedWith("Owner: can not be null address"); + }); + + it("Owner functions work as expected", async () => { + let tx = await contracts.bendExchange.connect(env.admin).updateCurrencyManager(contracts.currencyManager.address); + await expect(tx) + .to.emit(contracts.bendExchange, "NewCurrencyManager") + .withArgs(contracts.currencyManager.address); + + tx = await contracts.bendExchange.connect(env.admin).updateExecutionManager(contracts.executionManager.address); + await expect(tx) + .to.emit(contracts.bendExchange, "NewExecutionManager") + .withArgs(contracts.executionManager.address); + + tx = await contracts.bendExchange.connect(env.admin).updateRoyaltyFeeManager(contracts.royaltyFeeManager.address); + await expect(tx) + .to.emit(contracts.bendExchange, "NewRoyaltyFeeManager") + .withArgs(contracts.royaltyFeeManager.address); + + tx = await contracts.bendExchange.connect(env.admin).updateTransferManager(contracts.transferManager.address); + await expect(tx) + .to.emit(contracts.bendExchange, "NewTransferManager") + .withArgs(contracts.transferManager.address); + + tx = await contracts.bendExchange + .connect(env.admin) + .updateAuthorizationManager(contracts.authorizationManager.address); + await expect(tx) + .to.emit(contracts.bendExchange, "NewAuthorizationManager") + .withArgs(contracts.authorizationManager.address); + + tx = await contracts.bendExchange + .connect(env.admin) + .updateInterceptorManager(contracts.interceptorManager.address); + await expect(tx) + .to.emit(contracts.bendExchange, "NewInterceptorManager") + .withArgs(contracts.interceptorManager.address); + + tx = await contracts.bendExchange.connect(env.admin).updateProtocolFeeRecipient(env.admin.address); + await expect(tx).to.emit(contracts.bendExchange, "NewProtocolFeeRecipient").withArgs(env.admin.address); + }); + + it("Owner functions are only callable by owner", async () => { + const notAdminUser = env.accounts[3]; + + await expect( + contracts.bendExchange.connect(notAdminUser).updateCurrencyManager(contracts.currencyManager.address) + ).to.be.revertedWith("Ownable: caller is not the owner"); + + await expect( + contracts.bendExchange.connect(notAdminUser).updateExecutionManager(contracts.executionManager.address) + ).to.be.revertedWith("Ownable: caller is not the owner"); + + await expect( + contracts.bendExchange.connect(notAdminUser).updateProtocolFeeRecipient(notAdminUser.address) + ).to.be.revertedWith("Ownable: caller is not the owner"); + + await expect( + contracts.bendExchange.connect(notAdminUser).updateRoyaltyFeeManager(contracts.royaltyFeeManager.address) + ).to.be.revertedWith("Ownable: caller is not the owner"); + + await expect( + contracts.bendExchange.connect(notAdminUser).updateTransferManager(contracts.transferManager.address) + ).to.be.revertedWith("Ownable: caller is not the owner"); + + await expect( + contracts.bendExchange.connect(notAdminUser).updateAuthorizationManager(contracts.authorizationManager.address) + ).to.be.revertedWith("Ownable: caller is not the owner"); + await expect( + contracts.bendExchange.connect(notAdminUser).updateInterceptorManager(contracts.interceptorManager.address) + ).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); +}); diff --git a/test/CurrencyManager.test.ts b/test/CurrencyManager.test.ts new file mode 100644 index 0000000..10e03c2 --- /dev/null +++ b/test/CurrencyManager.test.ts @@ -0,0 +1,51 @@ +import { assert, expect } from "chai"; +import { BigNumber, constants } from "ethers"; +import { Contracts, Env, makeSuite } from "./_setup"; + +makeSuite("CurrencyManager", (contracts: Contracts, env: Env) => { + it("Revertions work as expected", async () => { + await expect(contracts.currencyManager.connect(env.admin).addCurrency(contracts.weth.address)).to.be.revertedWith( + "Currency: already whitelisted" + ); + + await expect( + contracts.currencyManager.connect(env.admin).removeCurrency(contracts.mockUSDT.address) + ).to.be.revertedWith("Currency: not whitelisted"); + }); + + it("Owner revertions work as expected", async () => { + await expect(contracts.currencyManager.connect(env.admin).addCurrency(constants.AddressZero)).to.be.revertedWith( + "Currency: can not be null address" + ); + + await expect(contracts.currencyManager.connect(env.admin).removeCurrency(constants.AddressZero)).to.be.revertedWith( + "Currency: not whitelisted" + ); + }); + it("Owner functions are only callable by owner", async () => { + const notAdminUser = env.accounts[3]; + + await expect( + contracts.currencyManager.connect(notAdminUser).addCurrency(contracts.mockUSDT.address) + ).to.be.revertedWith("Ownable: caller is not the owner"); + + await expect( + contracts.currencyManager.connect(notAdminUser).removeCurrency(contracts.weth.address) + ).to.be.revertedWith("Ownable: caller is not the owner"); + }); + it("View functions work as expected", async () => { + // Add a 2nd currency + await contracts.currencyManager.connect(env.admin).addCurrency(contracts.mockUSDT.address); + + const numberCurrencies = await contracts.currencyManager.viewCountWhitelistedCurrencies(); + assert.equal(numberCurrencies.toString(), "2"); + + let tx = await contracts.currencyManager.viewWhitelistedCurrencies("0", "1"); + assert.equal(tx[0].length, 1); + assert.deepEqual(BigNumber.from(tx[1].toString()), constants.One); + + tx = await contracts.currencyManager.viewWhitelistedCurrencies("1", "100"); + assert.equal(tx[0].length, 1); + assert.deepEqual(BigNumber.from(tx[1].toString()), BigNumber.from(numberCurrencies.toString())); + }); +}); diff --git a/test/ExecutionManager.test.ts b/test/ExecutionManager.test.ts new file mode 100644 index 0000000..42112ae --- /dev/null +++ b/test/ExecutionManager.test.ts @@ -0,0 +1,40 @@ +import { assert, expect } from "chai"; +import { BigNumber, constants } from "ethers"; +import { Contracts, Env, makeSuite } from "./_setup"; + +makeSuite("ExecutionManager", (contracts: Contracts, env: Env) => { + it("Revertions work as expected", async () => { + await expect( + contracts.executionManager.connect(env.admin).addStrategy(contracts.strategyPrivateSale.address) + ).to.be.revertedWith("Strategy: already whitelisted"); + + // MockUSDT is obviously not a strategy but this checks only if the address is in enumerable set + await expect( + contracts.executionManager.connect(env.admin).removeStrategy(contracts.mockUSDT.address) + ).to.be.revertedWith("Strategy: not whitelisted"); + }); + + it("Owner functions are only callable by owner", async () => { + const notAdminUser = env.accounts[3]; + + await expect( + contracts.executionManager.connect(notAdminUser).addStrategy(contracts.strategyPrivateSale.address) + ).to.be.revertedWith("Ownable: caller is not the owner"); + + await expect( + contracts.executionManager.connect(notAdminUser).removeStrategy(contracts.strategyPrivateSale.address) + ).to.be.revertedWith("Ownable: caller is not the owner"); + }); + it("View functions work as expected", async () => { + const numberStrategies = await contracts.executionManager.viewCountWhitelistedStrategies(); + assert.equal(numberStrategies.toString(), "5"); + + let tx = await contracts.executionManager.viewWhitelistedStrategies("0", "2"); + assert.equal(tx[0].length, 2); + assert.deepEqual(BigNumber.from(tx[1].toString()), constants.Two); + + tx = await contracts.executionManager.viewWhitelistedStrategies("2", "100"); + assert.equal(tx[0].length, 3); + assert.deepEqual(BigNumber.from(tx[1].toString()), BigNumber.from(numberStrategies.toString())); + }); +}); diff --git a/test/InterceptorManager.test.ts b/test/InterceptorManager.test.ts new file mode 100644 index 0000000..6fe4143 --- /dev/null +++ b/test/InterceptorManager.test.ts @@ -0,0 +1,41 @@ +import { assert, expect } from "chai"; +import { BigNumber, constants } from "ethers"; +import { Contracts, Env, makeSuite } from "./_setup"; + +makeSuite("InterceptorManager", (contracts: Contracts, env: Env) => { + it("Revertions work as expected", async () => { + await expect( + contracts.interceptorManager.connect(env.admin).addCollectionInterceptor(contracts.redeemNFT.address) + ).to.be.revertedWith("Interceptor: already whitelisted"); + + await expect( + contracts.interceptorManager.connect(env.admin).addCollectionInterceptor(constants.AddressZero) + ).to.be.revertedWith("Interceptor: can not be null address"); + + await expect( + contracts.interceptorManager.connect(env.admin).removeCollectionInterceptor(constants.AddressZero) + ).to.be.revertedWith("Interceptor: not whitelisted"); + }); + it("Owner functions are only callable by owner", async () => { + const notAdminUser = env.accounts[3]; + + await expect( + contracts.interceptorManager.connect(notAdminUser).addCollectionInterceptor(contracts.transferERC721.address) + ).to.be.revertedWith("Ownable: caller is not the owner"); + + await expect( + contracts.interceptorManager.connect(notAdminUser).removeCollectionInterceptor(contracts.transferERC721.address) + ).to.be.revertedWith("Ownable: caller is not the owner"); + }); + it("View functions work as expected", async () => { + const numberInterceptors = await contracts.interceptorManager.viewCountWhitelistedInterceptors(); + assert.equal(numberInterceptors.toString(), "1"); + + let tx = await contracts.interceptorManager.viewWhitelistedInterceptors("0", "1"); + assert.equal(tx[0].length, 1); + assert.deepEqual(BigNumber.from(tx[1].toString()), constants.One); + + tx = await contracts.interceptorManager.viewWhitelistedInterceptors("1", "100"); + assert.equal(tx[0].length, 0); + }); +}); diff --git a/test/RedeemNFT.test.ts b/test/RedeemNFT.test.ts new file mode 100644 index 0000000..76f6809 --- /dev/null +++ b/test/RedeemNFT.test.ts @@ -0,0 +1,1125 @@ +import { assert, expect } from "chai"; +import { BigNumber, BytesLike, constants, utils } from "ethers"; +import { ethers } from "hardhat"; + +import { MakerOrderWithSignature } from "./helpers/order-types"; +import { createMakerOrder, createTakerOrder } from "./helpers/order-helper"; +import { computeOrderHash } from "./helpers/signature-helper"; +import { Contracts, Env, makeSuite, Snapshots } from "./_setup"; +import { MockLendPool } from "../typechain"; +import { gasCost } from "./helpers/gas-helper"; + +const { defaultAbiCoder, parseEther } = utils; + +makeSuite("RedeemNFT", (contracts: Contracts, env: Env, snapshots: Snapshots) => { + let startTimeOrder: BigNumber; + let endTimeOrder: BigNumber; + let encodedMockLendPool: BytesLike; + let mockLendPool: MockLendPool; + const emptyEncodedBytes = defaultAbiCoder.encode([], []); + + before(async () => { + await contracts.weth.connect(env.admin).deposit({ value: parseEther("30") }); + const mockLendPoolAddress = await contracts.mockLendPoolAddressesProvider.getLendPool(); + await contracts.weth.connect(env.admin).transfer(mockLendPoolAddress, parseEther("30")); + + encodedMockLendPool = defaultAbiCoder.encode(["address"], [contracts.mockLendPoolAddressesProvider.address]); + const mockLendPoolFactory = await ethers.getContractFactory("MockLendPool"); + mockLendPool = mockLendPoolFactory.attach(mockLendPoolAddress); + await snapshots.capture("RedeemNFT"); + }); + + beforeEach(async () => { + // Set up defaults startTime/endTime (for orders) + startTimeOrder = BigNumber.from((await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp); + endTimeOrder = startTimeOrder.add(BigNumber.from("1000")); + }); + + afterEach(async () => { + await snapshots.revert("RedeemNFT"); + }); + + describe("#1 - Nft own hold", async () => { + it("Order NFT/ETH - MakerAsk order is matched by TakerBid order", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: contracts.redeemNFT.address, + interceptorExtra: encodedMockLendPool, + collection: contracts.mockERC721.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + price: parseEther("3"), + tokenId: constants.Zero, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const expectedFeeBalanceInWETH = parseEther("3").mul(env.standardProtocolFee.toNumber()).div(10000); + + const expectedTakerBalanceInETH = (await ethers.provider.getBalance(takerBidUser.address)).sub(parseEther("3")); + + const expectedMakerBalanceInWETH = (await contracts.weth.balanceOf(makerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH); + + const tx = await contracts.bendExchange + .connect(takerBidUser) + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + makerAskOrder.tokenId, + makerAskOrder.amount, + takerBidOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), takerBidUser.address); + + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + assert.deepEqual(expectedMakerBalanceInWETH, await contracts.weth.balanceOf(makerAskUser.address)); + assert.deepEqual( + expectedTakerBalanceInETH.sub(await gasCost(tx)), + await ethers.provider.getBalance(takerBidUser.address) + ); + + // Orders that have been executed cannot be matched again + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Order: matching order expired"); + }); + it("Order NFT/WETH - MakerBid order is matched by TakerAsk order", async () => { + const makerBidUser = env.accounts[2]; + const takerAskUser = env.accounts[1]; + const makerBidOrder = await createMakerOrder({ + isOrderAsk: false, + maker: makerBidUser.address, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + signerUser: makerBidUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerAskOrder = createTakerOrder({ + isOrderAsk: true, + taker: takerAskUser.address, + tokenId: constants.Zero, + price: makerBidOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: contracts.redeemNFT.address, + interceptorExtra: encodedMockLendPool, + }); + + const expectedMakerBalanceInWETH = (await contracts.weth.balanceOf(makerBidUser.address)).sub(parseEther("3")); + + const expectedFeeBalanceInWETH = parseEther("3").mul(env.standardProtocolFee.toNumber()).div(10000); + + const expectedTakerBalanceInWETH = (await contracts.weth.balanceOf(takerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH); + + const tx = await contracts.bendExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerAsk") + .withArgs( + computeOrderHash(makerBidOrder), + makerBidOrder.nonce, + takerAskUser.address, + makerBidUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerBidOrder.currency, + makerBidOrder.collection, + takerAskOrder.tokenId, + makerBidOrder.amount, + makerBidOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), makerBidUser.address); + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerBidUser.address, makerBidOrder.nonce) + ); + assert.deepEqual(expectedTakerBalanceInWETH, await contracts.weth.balanceOf(takerAskUser.address)); + assert.deepEqual(expectedMakerBalanceInWETH, await contracts.weth.balanceOf(makerBidUser.address)); + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + }); + }); + + describe("#2 - Nft non auction", async () => { + it("Order NFT/ETH - MakerAsk order is matched by TakerBid order", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + await contracts.mockERC721.connect(makerAskUser).setApprovalForAll(mockLendPool.address, true); + await mockLendPool + .connect(makerAskUser) + .borrow( + contracts.weth.address, + parseEther("1"), + contracts.mockERC721.address, + constants.Zero, + makerAskUser.address, + 0 + ); + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: contracts.redeemNFT.address, + interceptorExtra: encodedMockLendPool, + collection: contracts.mockERC721.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + price: parseEther("3"), + tokenId: constants.Zero, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const expectedFeeBalanceInWETH = parseEther("3").mul(env.standardProtocolFee.toNumber()).div(10000); + + const expectedTakerBalanceInETH = (await ethers.provider.getBalance(takerBidUser.address)).sub(parseEther("3")); + + const expectedMakerBalanceInWETH = (await contracts.weth.balanceOf(makerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH) + .sub(parseEther("1")); + + const tx = await contracts.bendExchange + .connect(takerBidUser) + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + makerAskOrder.tokenId, + makerAskOrder.amount, + takerBidOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), takerBidUser.address); + + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + assert.deepEqual(expectedMakerBalanceInWETH, await contracts.weth.balanceOf(makerAskUser.address)); + assert.deepEqual( + expectedTakerBalanceInETH.sub(await gasCost(tx)), + await ethers.provider.getBalance(takerBidUser.address) + ); + + // Orders that have been executed cannot be matched again + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Order: matching order expired"); + }); + + it("Order NFT/(ETH + WETH) - MakerAsk order is matched by TakerBid order", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + await contracts.mockERC721.connect(makerAskUser).setApprovalForAll(mockLendPool.address, true); + await mockLendPool + .connect(makerAskUser) + .borrow( + contracts.weth.address, + parseEther("1"), + contracts.mockERC721.address, + constants.Zero, + makerAskUser.address, + 0 + ); + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: contracts.redeemNFT.address, + interceptorExtra: encodedMockLendPool, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: constants.Zero, + price: parseEther("3"), + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + // Order is worth 3 ETH; taker user splits it as 2 ETH + 1 WETH + const expectedTakerBalanceInWETH = (await contracts.weth.balanceOf(takerBidUser.address)).sub(parseEther("1")); + + const expectedFeeBalanceInWETH = parseEther("3").mul(env.standardProtocolFee.toNumber()).div(10000); + + const expectedTakerBalanceInETH = (await ethers.provider.getBalance(takerBidUser.address)).sub(parseEther("2")); + + const expectedMakerBalanceInWETH = (await contracts.weth.balanceOf(makerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH) + .sub(parseEther("1")); + + const tx = await contracts.bendExchange + .connect(takerBidUser) + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: parseEther("2"), + }); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + makerAskOrder.tokenId, + makerAskOrder.amount, + takerBidOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), takerBidUser.address); + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + + // Check balance of WETH is same as expected + assert.deepEqual(expectedTakerBalanceInWETH, await contracts.weth.balanceOf(takerBidUser.address)); + assert.deepEqual( + expectedTakerBalanceInETH.sub(await gasCost(tx)), + await ethers.provider.getBalance(takerBidUser.address) + ); + assert.deepEqual(expectedMakerBalanceInWETH, await contracts.weth.balanceOf(makerAskUser.address)); + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + }); + + it("Order NFT/WETH - MakerBid order is matched by TakerAsk order", async () => { + const makerBidUser = env.accounts[2]; + const takerAskUser = env.accounts[1]; + await contracts.mockERC721.connect(takerAskUser).setApprovalForAll(mockLendPool.address, true); + await mockLendPool + .connect(takerAskUser) + .borrow( + contracts.weth.address, + parseEther("1"), + contracts.mockERC721.address, + constants.Zero, + takerAskUser.address, + 0 + ); + const makerBidOrder = await createMakerOrder({ + isOrderAsk: false, + maker: makerBidUser.address, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + signerUser: makerBidUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerAskOrder = createTakerOrder({ + isOrderAsk: true, + taker: takerAskUser.address, + tokenId: constants.Zero, + price: makerBidOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: contracts.redeemNFT.address, + interceptorExtra: encodedMockLendPool, + }); + + const expectedMakerBalanceInWETH = (await contracts.weth.balanceOf(makerBidUser.address)).sub(parseEther("3")); + + const expectedFeeBalanceInWETH = parseEther("3").mul(env.standardProtocolFee.toNumber()).div(10000); + + const expectedTakerBalanceInWETH = (await contracts.weth.balanceOf(takerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH) + .sub(parseEther("1")); + + const tx = await contracts.bendExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerAsk") + .withArgs( + computeOrderHash(makerBidOrder), + makerBidOrder.nonce, + takerAskUser.address, + makerBidUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerBidOrder.currency, + makerBidOrder.collection, + takerAskOrder.tokenId, + makerBidOrder.amount, + makerBidOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), makerBidUser.address); + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerBidUser.address, makerBidOrder.nonce) + ); + assert.deepEqual(expectedTakerBalanceInWETH, await contracts.weth.balanceOf(takerAskUser.address)); + assert.deepEqual(expectedMakerBalanceInWETH, await contracts.weth.balanceOf(makerBidUser.address)); + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + }); + + it("Order NFT/(ETH + WETH) - MakerAsk order is matched by TakerBid order, maker require ETH", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + await contracts.mockERC721.connect(makerAskUser).setApprovalForAll(mockLendPool.address, true); + await mockLendPool + .connect(makerAskUser) + .borrow( + contracts.weth.address, + parseEther("1"), + contracts.mockERC721.address, + constants.Zero, + makerAskUser.address, + 0 + ); + + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: constants.AddressZero, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: contracts.redeemNFT.address, + interceptorExtra: encodedMockLendPool, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: constants.Zero, + price: parseEther("3"), + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const expectedFeeBalanceInWETH = parseEther("3").mul(env.standardProtocolFee.toNumber()).div(10000); + + // Order is worth 3 ETH; taker user splits it as 2 ETH + 1 WETH + const expectedTakerBalanceInWETH = (await contracts.weth.balanceOf(takerBidUser.address)).sub(parseEther("1")); + const expectedTakerBalanceInETH = (await ethers.provider.getBalance(takerBidUser.address)).sub(parseEther("2")); + + const expectedMakerBalanceInETH = (await ethers.provider.getBalance(makerAskUser.address)).add( + parseEther("3").sub(expectedFeeBalanceInWETH).sub(parseEther("1")) + ); + + const tx = await contracts.bendExchange + .connect(takerBidUser) + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: parseEther("2"), + }); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + makerAskOrder.tokenId, + makerAskOrder.amount, + takerBidOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), takerBidUser.address); + + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + + // Check balance is same as expected + assert.deepEqual(expectedTakerBalanceInWETH, await contracts.weth.balanceOf(takerBidUser.address)); + assert.deepEqual( + expectedTakerBalanceInETH.sub(await gasCost(tx)), + await ethers.provider.getBalance(takerBidUser.address) + ); + assert.deepEqual(expectedMakerBalanceInETH, await ethers.provider.getBalance(makerAskUser.address)); + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + }); + }); + describe("#3 - Nft in auction", async () => { + it("Order NFT/ETH - MakerAsk order is matched by TakerBid order", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + await contracts.mockERC721.connect(makerAskUser).setApprovalForAll(mockLendPool.address, true); + await mockLendPool + .connect(makerAskUser) + .borrow( + contracts.weth.address, + parseEther("1"), + contracts.mockERC721.address, + constants.Zero, + makerAskUser.address, + 0 + ); + await mockLendPool.setMockInAuction(contracts.mockERC721.address, constants.Zero, parseEther("0.2")); + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: contracts.redeemNFT.address, + interceptorExtra: encodedMockLendPool, + collection: contracts.mockERC721.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + price: parseEther("3"), + tokenId: constants.Zero, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const expectedFeeBalanceInWETH = parseEther("3").mul(env.standardProtocolFee.toNumber()).div(10000); + + const expectedTakerBalanceInETH = (await ethers.provider.getBalance(takerBidUser.address)).sub(parseEther("3")); + + const expectedMakerBalanceInWETH = (await contracts.weth.balanceOf(makerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH) + .sub(parseEther("1.2")); + + const tx = await contracts.bendExchange + .connect(takerBidUser) + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + makerAskOrder.tokenId, + makerAskOrder.amount, + takerBidOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), takerBidUser.address); + + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + assert.deepEqual(expectedMakerBalanceInWETH, await contracts.weth.balanceOf(makerAskUser.address)); + assert.deepEqual( + expectedTakerBalanceInETH.sub(await gasCost(tx)), + await ethers.provider.getBalance(takerBidUser.address) + ); + + // Orders that have been executed cannot be matched again + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Order: matching order expired"); + }); + + it("Order NFT/(ETH + WETH) - MakerAsk order is matched by TakerBid order", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + await contracts.mockERC721.connect(makerAskUser).setApprovalForAll(mockLendPool.address, true); + await mockLendPool + .connect(makerAskUser) + .borrow( + contracts.weth.address, + parseEther("1"), + contracts.mockERC721.address, + constants.Zero, + makerAskUser.address, + 0 + ); + await mockLendPool.setMockInAuction(contracts.mockERC721.address, constants.Zero, parseEther("0.2")); + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: contracts.redeemNFT.address, + interceptorExtra: encodedMockLendPool, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: constants.Zero, + price: parseEther("3"), + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + // Order is worth 3 ETH; taker user splits it as 2 ETH + 1 WETH + const expectedTakerBalanceInWETH = (await contracts.weth.balanceOf(takerBidUser.address)).sub(parseEther("1")); + + const expectedFeeBalanceInWETH = parseEther("3").mul(env.standardProtocolFee.toNumber()).div(10000); + + const expectedTakerBalanceInETH = (await ethers.provider.getBalance(takerBidUser.address)).sub(parseEther("2")); + + const expectedMakerBalanceInWETH = (await contracts.weth.balanceOf(makerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH) + .sub(parseEther("1.2")); + + const tx = await contracts.bendExchange + .connect(takerBidUser) + + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: parseEther("2"), + }); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + makerAskOrder.tokenId, + makerAskOrder.amount, + takerBidOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), takerBidUser.address); + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + + // Check balance of WETH is same as expected + assert.deepEqual(expectedTakerBalanceInWETH, await contracts.weth.balanceOf(takerBidUser.address)); + assert.deepEqual( + expectedTakerBalanceInETH.sub(await gasCost(tx)), + await ethers.provider.getBalance(takerBidUser.address) + ); + assert.deepEqual(expectedMakerBalanceInWETH, await contracts.weth.balanceOf(makerAskUser.address)); + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + }); + + it("Order NFT/WETH - MakerBid order is matched by TakerAsk order", async () => { + const makerBidUser = env.accounts[2]; + const takerAskUser = env.accounts[1]; + await contracts.mockERC721.connect(takerAskUser).setApprovalForAll(mockLendPool.address, true); + await mockLendPool + .connect(takerAskUser) + .borrow( + contracts.weth.address, + parseEther("1"), + contracts.mockERC721.address, + constants.Zero, + takerAskUser.address, + 0 + ); + await mockLendPool.setMockInAuction(contracts.mockERC721.address, constants.Zero, parseEther("0.2")); + const makerBidOrder = await createMakerOrder({ + isOrderAsk: false, + maker: makerBidUser.address, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + signerUser: makerBidUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerAskOrder = createTakerOrder({ + isOrderAsk: true, + taker: takerAskUser.address, + tokenId: constants.Zero, + price: makerBidOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: contracts.redeemNFT.address, + interceptorExtra: encodedMockLendPool, + }); + + const expectedMakerBalanceInWETH = (await contracts.weth.balanceOf(makerBidUser.address)).sub(parseEther("3")); + + const expectedFeeBalanceInWETH = parseEther("3").mul(env.standardProtocolFee.toNumber()).div(10000); + + const expectedTakerBalanceInWETH = (await contracts.weth.balanceOf(takerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH) + .sub(parseEther("1.2")); + + const tx = await contracts.bendExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerAsk") + .withArgs( + computeOrderHash(makerBidOrder), + makerBidOrder.nonce, + takerAskUser.address, + makerBidUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerBidOrder.currency, + makerBidOrder.collection, + takerAskOrder.tokenId, + makerBidOrder.amount, + makerBidOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), makerBidUser.address); + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerBidUser.address, makerBidOrder.nonce) + ); + assert.deepEqual(expectedTakerBalanceInWETH, await contracts.weth.balanceOf(takerAskUser.address)); + assert.deepEqual(expectedMakerBalanceInWETH, await contracts.weth.balanceOf(makerBidUser.address)); + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + }); + + it("Order/BNFT/(ETH + WETH) - MakerAsk order with native ETH currency is matched by TakerBid order", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + await contracts.mockERC721.connect(makerAskUser).setApprovalForAll(mockLendPool.address, true); + await mockLendPool + .connect(makerAskUser) + .borrow( + contracts.weth.address, + parseEther("1"), + contracts.mockERC721.address, + constants.Zero, + makerAskUser.address, + 0 + ); + await mockLendPool.setMockInAuction(contracts.mockERC721.address, constants.Zero, parseEther("0.2")); + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: constants.AddressZero, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: contracts.redeemNFT.address, + interceptorExtra: encodedMockLendPool, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: constants.Zero, + price: parseEther("3"), + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const expectedFeeBalanceInWETH = parseEther("3").mul(env.standardProtocolFee.toNumber()).div(10000); + + // Order is worth 3 ETH; taker user splits it as 2 ETH + 1 WETH + const expectedTakerBalanceInWETH = (await contracts.weth.balanceOf(takerBidUser.address)).sub(parseEther("1")); + const expectedTakerBalanceInETH = (await ethers.provider.getBalance(takerBidUser.address)).sub(parseEther("2")); + + const expectedMakerBalanceInETH = (await ethers.provider.getBalance(makerAskUser.address)) + .add(parseEther("3")) + .sub(expectedFeeBalanceInWETH) + .sub(parseEther("1.2")); + + const tx = await contracts.bendExchange + .connect(takerBidUser) + .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: parseEther("2"), + }); + + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyStandardSaleForFixedPrice.address, + makerAskOrder.currency, + makerAskOrder.collection, + makerAskOrder.tokenId, + makerAskOrder.amount, + takerBidOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), takerBidUser.address); + + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + + // Check balance is same as expected + assert.deepEqual(expectedTakerBalanceInWETH, await contracts.weth.balanceOf(takerBidUser.address)); + assert.deepEqual( + expectedTakerBalanceInETH.sub(await gasCost(tx)), + await ethers.provider.getBalance(takerBidUser.address) + ); + assert.deepEqual(expectedMakerBalanceInETH, await ethers.provider.getBalance(makerAskUser.address)); + assert.deepEqual(expectedFeeBalanceInWETH, await contracts.weth.balanceOf(env.feeRecipient.address)); + }); + }); + describe("#4 - Revertions", async () => { + it("Cannot trade if no NFT ownership", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: contracts.redeemNFT.address, + interceptorExtra: encodedMockLendPool, + collection: contracts.mockERC721.address, + price: parseEther("3"), + tokenId: BigNumber.from(3), + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + price: parseEther("3"), + tokenId: BigNumber.from(3), + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Interceptor: no BNFT"); + }); + it("Cannot trade if no BNFT ownership", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + const bendDAOUser = env.accounts[3]; + + await contracts.mockERC721.connect(bendDAOUser).setApprovalForAll(mockLendPool.address, true); + await mockLendPool + .connect(bendDAOUser) + .borrow( + contracts.weth.address, + parseEther("1"), + contracts.mockERC721.address, + constants.Two, + bendDAOUser.address, + 0 + ); + + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: contracts.redeemNFT.address, + interceptorExtra: encodedMockLendPool, + collection: contracts.mockERC721.address, + price: parseEther("3"), + tokenId: constants.Two, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + price: parseEther("3"), + tokenId: constants.Two, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Interceptor: not BNFT owner"); + }); + it("Cannot trade if order price < tatal debt", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + await contracts.mockERC721.connect(makerAskUser).setApprovalForAll(mockLendPool.address, true); + await mockLendPool + .connect(makerAskUser) + .borrow( + contracts.weth.address, + parseEther("3.1"), + contracts.mockERC721.address, + constants.Zero, + makerAskUser.address, + 0 + ); + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: contracts.redeemNFT.address, + interceptorExtra: encodedMockLendPool, + collection: contracts.mockERC721.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + price: parseEther("3"), + tokenId: constants.Zero, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Interceptor: insufficent to repay debt"); + }); + + it("Cannot trade if order price < (tatal debt + bid fine)", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + await contracts.mockERC721.connect(makerAskUser).setApprovalForAll(mockLendPool.address, true); + await mockLendPool + .connect(makerAskUser) + .borrow( + contracts.weth.address, + parseEther("2.9"), + contracts.mockERC721.address, + constants.Zero, + makerAskUser.address, + 0 + ); + await mockLendPool.setMockInAuction(contracts.mockERC721.address, constants.Zero, parseEther("0.2")); + + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: contracts.redeemNFT.address, + interceptorExtra: encodedMockLendPool, + collection: contracts.mockERC721.address, + price: parseEther("3"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyStandardSaleForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + price: parseEther("3"), + tokenId: constants.Zero, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Interceptor: insufficent to repay debt"); + }); + }); +}); diff --git a/test/RoyaltyFeeSetter.test.ts b/test/RoyaltyFeeSetter.test.ts new file mode 100644 index 0000000..91839c3 --- /dev/null +++ b/test/RoyaltyFeeSetter.test.ts @@ -0,0 +1,259 @@ +import { assert, expect } from "chai"; +import { constants } from "ethers"; +import { ethers } from "hardhat"; +import { Contracts, Env, makeSuite } from "./_setup"; + +makeSuite("RoyaltyFeeSetter/RoyaltyFeeRegistry", (contracts: Contracts, env: Env) => { + it("Owner can set the royalty fee", async () => { + const fee = "200"; + const MockERC721WithOwner = await ethers.getContractFactory("MockERC721WithOwner"); + const mockERC721WithOwner = await MockERC721WithOwner.deploy("Mock Ownable ERC721", "MOERC721"); + await mockERC721WithOwner.deployed(); + + await expect( + contracts.royaltyFeeSetter + .connect(env.admin) + .updateRoyaltyInfoForCollectionIfAdmin( + mockERC721WithOwner.address, + env.royaltyCollector.address, + env.royaltyCollector.address, + fee + ) + ).to.be.revertedWith("function selector was not recognized and there's no fallback function"); + + const tx = await contracts.royaltyFeeSetter + .connect(env.admin) + .updateRoyaltyInfoForCollectionIfOwner( + mockERC721WithOwner.address, + env.royaltyCollector.address, + env.royaltyCollector.address, + fee + ); + + await expect(tx) + .to.emit(contracts.royaltyFeeRegistry, "RoyaltyFeeUpdate") + .withArgs(mockERC721WithOwner.address, env.royaltyCollector.address, env.royaltyCollector.address, fee); + }); + it("Admin can set the royalty fee", async () => { + const fee = "200"; + const MockERC721WithAdmin = await ethers.getContractFactory("MockERC721WithAdmin"); + const mockERC721WithAdmin = await MockERC721WithAdmin.deploy("Mock Ownable ERC721", "MOERC721"); + await mockERC721WithAdmin.deployed(); + + let res = await contracts.royaltyFeeSetter.checkForCollectionSetter(mockERC721WithAdmin.address); + assert.equal(res[0], env.admin.address); + assert.equal(res[1].toString(), "3"); + + await expect( + contracts.royaltyFeeSetter + .connect(env.admin) + .updateRoyaltyInfoForCollectionIfOwner( + mockERC721WithAdmin.address, + env.royaltyCollector.address, + env.royaltyCollector.address, + fee + ) + ).to.be.revertedWith("function selector was not recognized and there's no fallback function"); + + const tx = await contracts.royaltyFeeSetter + .connect(env.admin) + .updateRoyaltyInfoForCollectionIfAdmin( + mockERC721WithAdmin.address, + env.royaltyCollector.address, + env.royaltyCollector.address, + "200" + ); + + await expect(tx) + .to.emit(contracts.royaltyFeeRegistry, "RoyaltyFeeUpdate") + .withArgs(mockERC721WithAdmin.address, env.royaltyCollector.address, env.royaltyCollector.address, fee); + + res = await contracts.royaltyFeeSetter.checkForCollectionSetter(mockERC721WithAdmin.address); + assert.equal(res[0], env.royaltyCollector.address); + assert.equal(res[1].toString(), "0"); + }); + + it("Owner cannot set the royalty fee if already set", async () => { + const MockERC721WithOwner = await ethers.getContractFactory("MockERC721WithOwner"); + const mockERC721WithOwner = await MockERC721WithOwner.deploy("Mock Ownable ERC721", "MOERC721"); + await mockERC721WithOwner.deployed(); + + let res = await contracts.royaltyFeeSetter.checkForCollectionSetter(mockERC721WithOwner.address); + assert.equal(res[0], env.admin.address); + assert.equal(res[1].toString(), "2"); + + await contracts.royaltyFeeSetter + .connect(env.admin) + .updateRoyaltyInfoForCollectionIfOwner( + mockERC721WithOwner.address, + env.royaltyCollector.address, + env.royaltyCollector.address, + "200" + ); + + await expect( + contracts.royaltyFeeSetter + .connect(env.admin) + .updateRoyaltyInfoForCollectionIfOwner( + mockERC721WithOwner.address, + env.royaltyCollector.address, + env.royaltyCollector.address, + "200" + ) + ).to.been.revertedWith("Setter: already set"); + + const tx = await contracts.royaltyFeeSetter + .connect(env.royaltyCollector) + .updateRoyaltyInfoForCollectionIfSetter( + mockERC721WithOwner.address, + env.royaltyCollector.address, + env.royaltyCollector.address, + "200" + ); + + await expect(tx) + .to.emit(contracts.royaltyFeeRegistry, "RoyaltyFeeUpdate") + .withArgs(mockERC721WithOwner.address, env.royaltyCollector.address, env.royaltyCollector.address, "200"); + + res = await contracts.royaltyFeeSetter.checkForCollectionSetter(mockERC721WithOwner.address); + assert.equal(res[0], env.royaltyCollector.address); + assert.equal(res[1].toString(), "0"); + }); + + it("No function selector if no admin()/owner() function", async () => { + const res = await contracts.royaltyFeeSetter.checkForCollectionSetter(contracts.mockERC721.address); + assert.equal(res[0], constants.AddressZero); + assert.equal(res[1].toString(), "4"); + + await expect( + contracts.royaltyFeeSetter + .connect(env.admin) + .updateRoyaltyInfoForCollectionIfOwner( + contracts.mockERC721.address, + env.admin.address, + env.royaltyCollector.address, + "200" + ) + ).to.be.revertedWith("function selector was not recognized and there's no fallback function"); + + await expect( + contracts.royaltyFeeSetter.updateRoyaltyInfoForCollectionIfAdmin( + contracts.mockERC721.address, + env.admin.address, + env.royaltyCollector.address, + "200" + ) + ).to.be.revertedWith("function selector was not recognized and there's no fallback function"); + }); + + it("Cannot adjust if not the setter", async () => { + await expect( + contracts.royaltyFeeSetter.updateRoyaltyInfoForCollectionIfSetter( + contracts.mockERC721.address, + env.admin.address, + env.royaltyCollector.address, + "200" + ) + ).to.be.revertedWith("Setter: not the setter"); + }); + + it("Cannot set a royalty fee too high", async () => { + await expect( + contracts.royaltyFeeSetter + .connect(env.admin) + .updateRoyaltyInfoForCollection( + contracts.mockERC721.address, + env.royaltyCollector.address, + env.royaltyCollector.address, + "9501" + ) + ).to.be.revertedWith("Registry: royalty fee too high"); + }); + + it("Cannot set a royalty fee if not compliant", async () => { + const MockNonCompliantERC721 = await ethers.getContractFactory("MockNonCompliantERC721"); + const mockNonCompliantERC721 = await MockNonCompliantERC721.deploy("Mock Bad ERC721", "MBERC721"); + await mockNonCompliantERC721.deployed(); + + await expect( + contracts.royaltyFeeSetter + .connect(env.admin) + .updateRoyaltyInfoForCollectionIfOwner( + mockNonCompliantERC721.address, + env.royaltyCollector.address, + env.royaltyCollector.address, + "500" + ) + ).to.be.revertedWith("Setter: not ERC721/ERC1155"); + }); + + it("Cannot set custom royalty fee if ERC2981", async () => { + const res = await contracts.royaltyFeeSetter.checkForCollectionSetter(contracts.mockERC721WithRoyalty.address); + + assert.equal(res[0], constants.AddressZero); + assert.equal(res[1].toString(), "1"); + + await expect( + contracts.royaltyFeeSetter + .connect(env.admin) + .updateRoyaltyInfoForCollectionIfOwner( + contracts.mockERC721WithRoyalty.address, + env.royaltyCollector.address, + env.royaltyCollector.address, + "500" + ) + ).to.be.revertedWith("Owner: must not be ERC2981"); + + await expect( + contracts.royaltyFeeSetter + .connect(env.admin) + .updateRoyaltyInfoForCollectionIfAdmin( + contracts.mockERC721WithRoyalty.address, + env.royaltyCollector.address, + env.royaltyCollector.address, + "500" + ) + ).to.be.revertedWith("Admin: must not be ERC2981"); + }); + + it("Owner functions work as expected", async () => { + let tx = await contracts.royaltyFeeSetter.connect(env.admin).updateRoyaltyFeeLimit("30"); + await expect(tx).to.emit(contracts.royaltyFeeRegistry, "NewRoyaltyFeeLimit").withArgs("30"); + + await expect(contracts.royaltyFeeSetter.connect(env.admin).updateRoyaltyFeeLimit("9501")).to.be.revertedWith( + "Owner: royalty fee limit too high" + ); + + tx = await contracts.royaltyFeeSetter.connect(env.admin).updateOwnerOfRoyaltyFeeRegistry(env.admin.address); + await expect(tx) + .to.emit(contracts.royaltyFeeRegistry, "OwnershipTransferred") + .withArgs(contracts.royaltyFeeSetter.address, env.admin.address); + }); + + it("Owner functions are only callable by owner", async () => { + const notAdminUser = env.accounts[3]; + + await expect(contracts.royaltyFeeRegistry.connect(notAdminUser).updateRoyaltyFeeLimit("30")).to.be.revertedWith( + "Ownable: caller is not the owner" + ); + + await expect( + contracts.royaltyFeeSetter + .connect(notAdminUser) + .updateRoyaltyInfoForCollection( + contracts.mockERC721.address, + notAdminUser.address, + notAdminUser.address, + "5000" + ) + ).to.be.revertedWith("Ownable: caller is not the owner"); + + await expect( + contracts.royaltyFeeSetter.connect(notAdminUser).updateOwnerOfRoyaltyFeeRegistry(notAdminUser.address) + ).to.be.revertedWith("Ownable: caller is not the owner"); + + await expect(contracts.royaltyFeeSetter.connect(notAdminUser).updateRoyaltyFeeLimit("10")).to.be.revertedWith( + "Ownable: caller is not the owner" + ); + }); +}); diff --git a/test/TransferManager.test.ts b/test/TransferManager.test.ts new file mode 100644 index 0000000..608b121 --- /dev/null +++ b/test/TransferManager.test.ts @@ -0,0 +1,37 @@ +import { expect } from "chai"; +import { constants } from "ethers"; +import { Contracts, Env, makeSuite } from "./_setup"; + +makeSuite("TransferManager", (contracts: Contracts, env: Env) => { + it("Owner revertions work as expected", async () => { + await expect( + contracts.transferManager + .connect(env.admin) + .addCollectionTransfer(contracts.mockERC721.address, constants.AddressZero) + ).to.be.revertedWith("Owner: transfer cannot be null address"); + + await expect( + contracts.transferManager + .connect(env.admin) + .addCollectionTransfer(constants.AddressZero, contracts.transferERC721.address) + ).to.be.revertedWith("Owner: collection cannot be null address"); + + await expect( + contracts.transferManager.connect(env.admin).removeCollectionTransfer(contracts.mockERC721.address) + ).to.be.revertedWith("Owner: collection has no transfer"); + }); + + it("Owner functions are only callable by owner", async () => { + const notAdminUser = env.accounts[3]; + + await expect( + contracts.transferManager + .connect(notAdminUser) + .addCollectionTransfer(contracts.mockERC721WithRoyalty.address, contracts.transferERC721.address) + ).to.be.revertedWith("Ownable: caller is not the owner"); + + await expect( + contracts.transferManager.connect(notAdminUser).removeCollectionTransfer(contracts.mockERC721WithRoyalty.address) + ).to.be.revertedWith("Ownable: caller is not the owner"); + }); +}); diff --git a/test/_setup.ts b/test/_setup.ts new file mode 100644 index 0000000..dcbe73b --- /dev/null +++ b/test/_setup.ts @@ -0,0 +1,339 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { BigNumber, constants } from "ethers"; +import { ethers } from "hardhat"; +import { assert } from "chai"; +import { defaultAbiCoder, parseEther } from "ethers/lib/utils"; +import { + AuthorizationManager, + BendExchange, + CurrencyManager, + ExecutionManager, + MockERC1155, + MockERC20, + MockERC721, + MockERC721WithRoyalty, + RoyaltyFeeManager, + RoyaltyFeeRegistry, + RoyaltyFeeSetter, + StrategyAnyItemFromCollectionForFixedPrice, + StrategyAnyItemInASetForFixedPrice, + StrategyDutchAuction, + StrategyPrivateSale, + StrategyStandardSaleForFixedPrice, + TransferERC1155, + RedeemNFT, + TransferERC721, + TransferManager, + InterceptorManager, + TransferNonCompliantERC721, + MockLendPoolAddressesProvider, + WETH, +} from "../typechain"; +import { computeDomainSeparator } from "./helpers/signature-helper"; + +export interface Contracts { + initialized: boolean; + weth: WETH; + mockERC721: MockERC721; + mockERC1155: MockERC1155; + mockUSDT: MockERC20; + mockERC721WithRoyalty: MockERC721WithRoyalty; + currencyManager: CurrencyManager; + executionManager: ExecutionManager; + authorizationManager: AuthorizationManager; + interceptorManager: InterceptorManager; + transferManager: TransferManager; + transferERC721: TransferERC721; + transferNonCompliantERC721: TransferNonCompliantERC721; + redeemNFT: RedeemNFT; + transferERC1155: TransferERC1155; + strategyStandardSaleForFixedPrice: StrategyStandardSaleForFixedPrice; + strategyAnyItemFromCollectionForFixedPrice: StrategyAnyItemFromCollectionForFixedPrice; + strategyDutchAuction: StrategyDutchAuction; + strategyPrivateSale: StrategyPrivateSale; + strategyAnyItemInASetForFixedPrice: StrategyAnyItemInASetForFixedPrice; + royaltyFeeRegistry: RoyaltyFeeRegistry; + royaltyFeeManager: RoyaltyFeeManager; + royaltyFeeSetter: RoyaltyFeeSetter; + bendExchange: BendExchange; + mockLendPoolAddressesProvider: MockLendPoolAddressesProvider; +} + +export async function tokenSetUp(users: SignerWithAddress[], contracts: Contracts): Promise { + for (const user of users) { + // Each user gets 30 WETH + await contracts.weth.connect(user).deposit({ value: parseEther("30") }); + + // register proxy + await contracts.authorizationManager.connect(user).registerProxy(); + const userProxy = await contracts.authorizationManager.proxies(user.address); + + // Set approval for WETH + await contracts.weth.connect(user).approve(userProxy, constants.MaxUint256); + + // Each user mints 1 ERC721 NFT + await contracts.mockERC721.connect(user).mint(user.address); + + // Set approval for all tokens in mock collection to user proxy for ERC721 + await contracts.mockERC721.connect(user).setApprovalForAll(userProxy, true); + + // Each user mints 1 ERC721WithRoyalty NFT + await contracts.mockERC721WithRoyalty.connect(user).mint(user.address); + + // Set approval for all tokens in mock collection to user proxy for ERC721WithRoyalty + await contracts.mockERC721WithRoyalty.connect(user).setApprovalForAll(userProxy, true); + + // Each user batch mints 2 ERC1155 for tokenIds 1, 2, 3 + await contracts.mockERC1155 + .connect(user) + .mintBatch(user.address, ["1", "2", "3"], ["2", "2", "2"], defaultAbiCoder.encode([], [])); + + // Set approval for all tokens in mock collection to transferManager contract for ERC1155 + await contracts.mockERC1155.connect(user).setApprovalForAll(userProxy, true); + } +} + +export async function setUp( + admin: SignerWithAddress, + feeRecipient: SignerWithAddress, + royaltyCollector: SignerWithAddress, + standardProtocolFee: BigNumber, + royaltyFeeLimit: BigNumber +): Promise { + /** 1. Deploy WETH, Mock ERC721, Mock ERC1155, Mock USDT, MockERC721WithRoyalty + */ + const WETH = await ethers.getContractFactory("WETH"); + const weth = await WETH.deploy(); + await weth.deployed(); + const MockERC721 = await ethers.getContractFactory("MockERC721"); + const mockERC721 = await MockERC721.deploy("Mock ERC721", "MERC721"); + await mockERC721.deployed(); + const MockERC1155 = await ethers.getContractFactory("MockERC1155"); + const mockERC1155 = await MockERC1155.deploy("uri/"); + await mockERC1155.deployed(); + const MockERC20 = await ethers.getContractFactory("MockERC20"); + const mockUSDT = await MockERC20.deploy("USD Tether", "USDT"); + await mockUSDT.deployed(); + const MockERC721WithRoyalty = await ethers.getContractFactory("MockERC721WithRoyalty"); + const mockERC721WithRoyalty = await MockERC721WithRoyalty.connect(royaltyCollector).deploy( + "Mock Royalty ERC721", + "MRC721", + "200" // 2% royalty fee + ); + await mockERC721WithRoyalty.deployed(); + + /** 2. Deploy CurrencyManager contract and add WETH to whitelisted currencies + */ + const CurrencyManager = await ethers.getContractFactory("CurrencyManager"); + const currencyManager = await CurrencyManager.deploy(); + await currencyManager.deployed(); + await currencyManager.connect(admin).addCurrency(weth.address); + + /** 3. Deploy ExecutionManager contract + */ + const ExecutionManager = await ethers.getContractFactory("ExecutionManager"); + const executionManager = await ExecutionManager.deploy(); + await executionManager.deployed(); + + /** 4. Deploy execution strategy contracts for trade execution + */ + const StrategyAnyItemFromCollectionForFixedPrice = await ethers.getContractFactory( + "StrategyAnyItemFromCollectionForFixedPrice" + ); + const strategyAnyItemFromCollectionForFixedPrice = await StrategyAnyItemFromCollectionForFixedPrice.deploy(200); + await strategyAnyItemFromCollectionForFixedPrice.deployed(); + const StrategyAnyItemInASetForFixedPrice = await ethers.getContractFactory("StrategyAnyItemInASetForFixedPrice"); + const strategyAnyItemInASetForFixedPrice = await StrategyAnyItemInASetForFixedPrice.deploy(standardProtocolFee); + await strategyAnyItemInASetForFixedPrice.deployed(); + const StrategyDutchAuction = await ethers.getContractFactory("StrategyDutchAuction"); + const strategyDutchAuction = await StrategyDutchAuction.deploy( + standardProtocolFee, + BigNumber.from("900") // 15 minutes + ); + await strategyDutchAuction.deployed(); + const StrategyPrivateSale = await ethers.getContractFactory("StrategyPrivateSale"); + const strategyPrivateSale = await StrategyPrivateSale.deploy(constants.Zero); + await strategyPrivateSale.deployed(); + const StrategyStandardSaleForFixedPrice = await ethers.getContractFactory("StrategyStandardSaleForFixedPrice"); + const strategyStandardSaleForFixedPrice = await StrategyStandardSaleForFixedPrice.deploy(standardProtocolFee); + await strategyStandardSaleForFixedPrice.deployed(); + + // Whitelist these five strategies + await executionManager.connect(admin).addStrategy(strategyStandardSaleForFixedPrice.address); + await executionManager.connect(admin).addStrategy(strategyAnyItemFromCollectionForFixedPrice.address); + await executionManager.connect(admin).addStrategy(strategyAnyItemInASetForFixedPrice.address); + await executionManager.connect(admin).addStrategy(strategyDutchAuction.address); + await executionManager.connect(admin).addStrategy(strategyPrivateSale.address); + + /** 5. Deploy RoyaltyFee Registry/Setter/Manager + */ + const RoyaltyFeeRegistry = await ethers.getContractFactory("RoyaltyFeeRegistry"); + const royaltyFeeRegistry = await RoyaltyFeeRegistry.deploy(royaltyFeeLimit); + await royaltyFeeRegistry.deployed(); + const RoyaltyFeeSetter = await ethers.getContractFactory("RoyaltyFeeSetter"); + const royaltyFeeSetter = await RoyaltyFeeSetter.deploy(royaltyFeeRegistry.address); + await royaltyFeeSetter.deployed(); + const RoyaltyFeeManager = await ethers.getContractFactory("RoyaltyFeeManager"); + const royaltyFeeManager = await RoyaltyFeeManager.deploy(royaltyFeeRegistry.address); + await royaltyFeeSetter.deployed(); + // Transfer ownership of RoyaltyFeeRegistry to RoyaltyFeeSetter + await royaltyFeeRegistry.connect(admin).transferOwnership(royaltyFeeSetter.address); + + /** 6. Deploy TransferManager and transfers + */ + + const TransferERC721 = await ethers.getContractFactory("TransferERC721"); + const transferERC721 = await TransferERC721.deploy(); + await transferERC721.deployed(); + + const TransferERC1155 = await ethers.getContractFactory("TransferERC1155"); + const transferERC1155 = await TransferERC1155.deploy(); + await transferERC1155.deployed(); + + const TransferNonCompliantERC721 = await ethers.getContractFactory("TransferNonCompliantERC721"); + const transferNonCompliantERC721 = await TransferNonCompliantERC721.deploy(); + await transferNonCompliantERC721.deployed(); + + const TransferManager = await ethers.getContractFactory("TransferManager"); + const transferManager = await TransferManager.deploy(transferERC721.address, transferERC1155.address); + await transferManager.deployed(); + + const InterceptorManager = await ethers.getContractFactory("InterceptorManager"); + const interceptorManager = await InterceptorManager.deploy(); + await interceptorManager.deployed(); + + const RedeemNFT = await ethers.getContractFactory("RedeemNFT"); + const redeemNFT = await RedeemNFT.deploy(); + await redeemNFT.deployed(); + + // Whitelist before transfers + await interceptorManager.connect(admin).addCollectionInterceptor(redeemNFT.address); + + /** 7. Deploy BendExchange contract + */ + const BendExchange = await ethers.getContractFactory("BendExchange"); + const bendExchange = await BendExchange.deploy( + interceptorManager.address, + transferManager.address, + currencyManager.address, + executionManager.address, + royaltyFeeManager.address, + weth.address, + feeRecipient.address + ); + await bendExchange.deployed(); + + /** 8. Deploy AuthorizationManager contract + */ + + const AuthorizationManager = await ethers.getContractFactory("AuthorizationManager"); + const authorizationManager = await AuthorizationManager.deploy(weth.address, bendExchange.address); + await authorizationManager.deployed(); + await bendExchange.updateAuthorizationManager(authorizationManager.address); + + /** 9. Deploy MockLendPool contract + */ + + const MockLendPoolAddressesProvider = await ethers.getContractFactory("MockLendPoolAddressesProvider"); + const mockLendPoolAddressesProvider = await MockLendPoolAddressesProvider.deploy(); + await mockLendPoolAddressesProvider.deployed(); + + /** Return contracts + */ + return { + initialized: true, + weth, + mockLendPoolAddressesProvider, + mockERC721, + mockERC1155, + mockUSDT, + mockERC721WithRoyalty, + currencyManager, + executionManager, + authorizationManager, + interceptorManager, + transferManager, + transferERC721, + transferNonCompliantERC721, + redeemNFT, + transferERC1155, + strategyStandardSaleForFixedPrice, + strategyAnyItemFromCollectionForFixedPrice, + strategyDutchAuction, + strategyPrivateSale, + strategyAnyItemInASetForFixedPrice, + royaltyFeeRegistry, + royaltyFeeManager, + royaltyFeeSetter, + bendExchange, + } as Contracts; +} + +export class Snapshots { + ids = new Map(); + + async capture(tag: string): Promise { + this.ids.set(tag, await this.evmSnapshot()); + } + + async revert(tag: string): Promise { + await this.evmRevert(this.ids.get(tag) || "1"); + await this.capture(tag); + } + + async evmSnapshot(): Promise { + return await ethers.provider.send("evm_snapshot", []); + } + + async evmRevert(id: string): Promise { + return await ethers.provider.send("evm_revert", [id]); + } +} +export interface Env { + initialized: boolean; + accounts: SignerWithAddress[]; + admin: SignerWithAddress; + feeRecipient: SignerWithAddress; + royaltyCollector: SignerWithAddress; + standardProtocolFee: BigNumber; + royaltyFeeLimit: BigNumber; // 95% +} + +const contracts: Contracts = { initialized: false } as Contracts; +const env: Env = { initialized: false } as Env; +const snapshots = new Snapshots(); +export function makeSuite(name: string, tests: (contracts: Contracts, env: Env, snapshots: Snapshots) => void): void { + describe(name, () => { + let _id: any; + before(async () => { + if (!env.initialized && !contracts.initialized) { + env.accounts = await ethers.getSigners(); + env.admin = env.accounts[0]; + env.feeRecipient = env.accounts[19]; + env.royaltyCollector = env.accounts[15]; + env.standardProtocolFee = BigNumber.from("200"); + env.royaltyFeeLimit = BigNumber.from("9500"); // 95% + + Object.assign( + contracts, + await setUp(env.admin, env.feeRecipient, env.royaltyCollector, env.standardProtocolFee, env.royaltyFeeLimit) + ); + await tokenSetUp(env.accounts.slice(1, 10), contracts); + env.initialized = true; + contracts.initialized = true; + // Verify the domain separator is properly computed + assert.equal( + await contracts.bendExchange.DOMAIN_SEPARATOR(), + computeDomainSeparator(contracts.bendExchange.address) + ); + snapshots.capture("setup"); + } + _id = await snapshots.evmSnapshot(); + }); + tests(contracts, env, snapshots); + after(async () => { + await snapshots.evmRevert(_id); + }); + }); +} diff --git a/test/helpers/gas-helper.ts b/test/helpers/gas-helper.ts new file mode 100644 index 0000000..86cd32f --- /dev/null +++ b/test/helpers/gas-helper.ts @@ -0,0 +1,6 @@ +import { BigNumber, ContractTransaction } from "ethers"; + +export async function gasCost(tx: ContractTransaction): Promise { + const receipt = await tx.wait(); + return receipt.cumulativeGasUsed.mul(receipt.effectiveGasPrice); +} diff --git a/test/helpers/hardhat-keys.ts b/test/helpers/hardhat-keys.ts index ed31f45..90cdac5 100644 --- a/test/helpers/hardhat-keys.ts +++ b/test/helpers/hardhat-keys.ts @@ -16,6 +16,9 @@ export function findPrivateKey(publicKey: string): string { case "0x15d34aaf54267db7d7c367839aaf71a00a2c6a65": return "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a"; + case "0x71be63f3384f5fb98995898a86b02fb2426c5788": + return "0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82"; + default: return "0x"; } diff --git a/test/helpers/order-helper.ts b/test/helpers/order-helper.ts index 27a5227..7df515e 100644 --- a/test/helpers/order-helper.ts +++ b/test/helpers/order-helper.ts @@ -9,7 +9,7 @@ export interface SignedMakerOrder extends MakerOrder { export async function createMakerOrder({ isOrderAsk, - signer, + maker, collection, price, tokenId, @@ -21,12 +21,14 @@ export async function createMakerOrder({ endTime, minPercentageToAsk, params, + interceptor, + interceptorExtra, signerUser, verifyingContract, }: SignedMakerOrder): Promise { const makerOrder: MakerOrder = { isOrderAsk: isOrderAsk, - signer: signer, + maker: maker, collection: collection, price: price, tokenId: tokenId, @@ -38,6 +40,8 @@ export async function createMakerOrder({ endTime: endTime, minPercentageToAsk: minPercentageToAsk, params: params, + interceptor, + interceptorExtra, }; const signedOrder = await signMakerOrder(signerUser, verifyingContract, makerOrder); @@ -60,6 +64,8 @@ export function createTakerOrder({ tokenId, minPercentageToAsk, params, + interceptor, + interceptorExtra, }: TakerOrder): TakerOrder { const takerOrder: TakerOrder = { isOrderAsk: isOrderAsk, @@ -68,6 +74,8 @@ export function createTakerOrder({ tokenId: tokenId, minPercentageToAsk: minPercentageToAsk, params: params, + interceptor, + interceptorExtra, }; return takerOrder; diff --git a/test/helpers/order-types.ts b/test/helpers/order-types.ts index b666266..393dd9b 100644 --- a/test/helpers/order-types.ts +++ b/test/helpers/order-types.ts @@ -2,7 +2,7 @@ import { BigNumber, BigNumberish, BytesLike } from "ethers"; export interface MakerOrder { isOrderAsk: boolean; // true if ask, false if bid - signer: string; // signer address of the maker order + maker: string; // address of the maker order collection: string; // collection address price: BigNumber; // price tokenId: BigNumber; // id of the token @@ -14,6 +14,8 @@ export interface MakerOrder { startTime: BigNumber; // startTime in epoch endTime: BigNumber; // endTime in epoch params: BytesLike; // additional parameters + interceptor: string; + interceptorExtra: BytesLike; } export interface MakerOrderWithSignature extends MakerOrder { @@ -29,4 +31,6 @@ export interface TakerOrder { tokenId: BigNumber; minPercentageToAsk: BigNumber; params: BytesLike; // params (e.g., tokenId) + interceptor: string; + interceptorExtra: BytesLike; } diff --git a/test/helpers/signature-helper.ts b/test/helpers/signature-helper.ts index 9f8efc1..d7468cf 100644 --- a/test/helpers/signature-helper.ts +++ b/test/helpers/signature-helper.ts @@ -16,7 +16,7 @@ const { defaultAbiCoder, keccak256, solidityPack } = utils; * @param signer signer * @param types solidity types of the value param * @param values params to be sent to the Solidity function - * @param verifyingContract verifying contract address ("LooksRareExchange") + * @param verifyingContract verifying contract address ("BendExchange") * @returns splitted signature * @see https://docs.ethers.io/v5/api/signer/#Signer-signTypedData */ @@ -27,7 +27,7 @@ const signTypedData = async ( verifyingContract: string ): Promise => { const domain: TypedDataDomain = { - name: "LooksRareExchange", + name: "BendExchange", version: "1", chainId: "31337", // HRE verifyingContract: verifyingContract, @@ -49,7 +49,7 @@ const signTypedData = async ( export const computeDomainSeparator = (verifyingContract: string): string => { const domain: TypedDataDomain = { - name: "LooksRareExchange", + name: "BendExchange", version: "1", chainId: "31337", // HRE verifyingContract: verifyingContract, @@ -78,12 +78,14 @@ export const computeOrderHash = (order: MakerOrder): string => { "uint256", "uint256", "bytes32", + "address", + "bytes32", ]; const values = [ - "0x40261ade532fa1d2c7293df30aaadb9b3c616fae525a0b56d3d411c841a85028", // maker order hash (from Solidity) + "0xfd561ac528d7d2fc669c32105ec4867617451ed5ca6ccde2e4ed234a0a41010a", // maker order hash (from Solidity) order.isOrderAsk, - order.signer, + order.maker, order.collection, order.price, order.tokenId, @@ -95,6 +97,8 @@ export const computeOrderHash = (order: MakerOrder): string => { order.endTime, order.minPercentageToAsk, keccak256(order.params), + order.interceptor, + keccak256(order.interceptorExtra), ]; return keccak256(defaultAbiCoder.encode(types, values)); @@ -127,12 +131,14 @@ export const signMakerOrder = ( "uint256", "uint256", "bytes32", + "address", + "bytes32", ]; const values = [ - "0x40261ade532fa1d2c7293df30aaadb9b3c616fae525a0b56d3d411c841a85028", + "0xfd561ac528d7d2fc669c32105ec4867617451ed5ca6ccde2e4ed234a0a41010a", order.isOrderAsk, - order.signer, + order.maker, order.collection, order.price, order.tokenId, @@ -144,6 +150,8 @@ export const signMakerOrder = ( order.endTime, order.minPercentageToAsk, keccak256(order.params), + order.interceptor, + keccak256(order.interceptorExtra), ]; return signTypedData(signer, types, values, verifyingContract); diff --git a/test/looksRareExchange.test.ts b/test/looksRareExchange.test.ts deleted file mode 100644 index 0030afe..0000000 --- a/test/looksRareExchange.test.ts +++ /dev/null @@ -1,2339 +0,0 @@ -import { assert, expect } from "chai"; -import { BigNumber, constants, Contract, utils } from "ethers"; -import { ethers } from "hardhat"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; - -import { increaseTo } from "./helpers/block-traveller"; -import { MakerOrderWithSignature, TakerOrder } from "./helpers/order-types"; -import { createMakerOrder, createTakerOrder } from "./helpers/order-helper"; -import { computeDomainSeparator, computeOrderHash } from "./helpers/signature-helper"; -import { setUp } from "./test-setup"; -import { tokenSetUp } from "./token-set-up"; - -const { defaultAbiCoder, parseEther } = utils; - -describe("LooksRare Exchange", () => { - // Mock contracts - let mockUSDT: Contract; - let mockERC721: Contract; - let mockERC721WithRoyalty: Contract; - let mockERC1155: Contract; - let weth: Contract; - - // Exchange contracts - let transferSelectorNFT: Contract; - let transferManagerERC721: Contract; - let transferManagerERC1155: Contract; - let transferManagerNonCompliantERC721: Contract; - let currencyManager: Contract; - let executionManager: Contract; - let royaltyFeeManager: Contract; - let royaltyFeeRegistry: Contract; - let royaltyFeeSetter: Contract; - let looksRareExchange: Contract; - - // Strategy contracts (used for this test file) - let strategyPrivateSale: Contract; - let strategyStandardSaleForFixedPrice: Contract; - - // Other global variables - let standardProtocolFee: BigNumber; - let royaltyFeeLimit: BigNumber; - let accounts: SignerWithAddress[]; - let admin: SignerWithAddress; - let feeRecipient: SignerWithAddress; - let royaltyCollector: SignerWithAddress; - let startTimeOrder: BigNumber; - let endTimeOrder: BigNumber; - - beforeEach(async () => { - accounts = await ethers.getSigners(); - admin = accounts[0]; - feeRecipient = accounts[19]; - royaltyCollector = accounts[15]; - standardProtocolFee = BigNumber.from("200"); - royaltyFeeLimit = BigNumber.from("9500"); // 95% - [ - weth, - mockERC721, - mockERC1155, - mockUSDT, - mockERC721WithRoyalty, - currencyManager, - executionManager, - transferSelectorNFT, - transferManagerERC721, - transferManagerERC1155, - transferManagerNonCompliantERC721, - looksRareExchange, - strategyStandardSaleForFixedPrice, - , - , - strategyPrivateSale, - , - royaltyFeeRegistry, - royaltyFeeManager, - royaltyFeeSetter, - ] = await setUp(admin, feeRecipient, royaltyCollector, standardProtocolFee, royaltyFeeLimit); - - await tokenSetUp( - accounts.slice(1, 10), - weth, - mockERC721, - mockERC721WithRoyalty, - mockERC1155, - looksRareExchange, - transferManagerERC721, - transferManagerERC1155 - ); - - // Verify the domain separator is properly computed - assert.equal(await looksRareExchange.DOMAIN_SEPARATOR(), computeDomainSeparator(looksRareExchange.address)); - - // Set up defaults startTime/endTime (for orders) - startTimeOrder = BigNumber.from((await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp); - endTimeOrder = startTimeOrder.add(BigNumber.from("1000")); - }); - - describe("#1 - Regular sales", async () => { - it("Standard Order/ERC721/ETH only - MakerAsk order is matched by TakerBid order", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - price: parseEther("3"), - tokenId: constants.Zero, - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder = createTakerOrder({ - isOrderAsk: false, - taker: takerBidUser.address, - price: parseEther("3"), - tokenId: constants.Zero, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - const tx = await looksRareExchange - .connect(takerBidUser) - .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { - value: takerBidOrder.price, - }); - - await expect(tx) - .to.emit(looksRareExchange, "TakerBid") - .withArgs( - computeOrderHash(makerAskOrder), - makerAskOrder.nonce, - takerBidUser.address, - makerAskUser.address, - strategyStandardSaleForFixedPrice.address, - makerAskOrder.currency, - makerAskOrder.collection, - makerAskOrder.tokenId, - makerAskOrder.amount, - takerBidOrder.price - ); - - assert.equal(await mockERC721.ownerOf("0"), takerBidUser.address); - - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) - ); - - // Orders that have been executed cannot be matched again - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { - value: takerBidOrder.price, - }) - ).to.be.revertedWith("Order: Matching order expired"); - }); - - it("Standard Order/ERC721/(ETH + WETH) - MakerAsk order is matched by TakerBid order", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder = createTakerOrder({ - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: constants.Zero, - price: parseEther("3"), - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - // Order is worth 3 ETH; taker user splits it as 2 ETH + 1 WETH - const expectedBalanceInWETH = BigNumber.from((await weth.balanceOf(takerBidUser.address)).toString()).sub( - BigNumber.from(parseEther("1")) - ); - - const tx = await looksRareExchange - .connect(takerBidUser) - .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { - value: parseEther("2"), - }); - - await expect(tx) - .to.emit(looksRareExchange, "TakerBid") - .withArgs( - computeOrderHash(makerAskOrder), - makerAskOrder.nonce, - takerBidUser.address, - makerAskUser.address, - strategyStandardSaleForFixedPrice.address, - makerAskOrder.currency, - makerAskOrder.collection, - makerAskOrder.tokenId, - makerAskOrder.amount, - takerBidOrder.price - ); - - assert.equal(await mockERC721.ownerOf("0"), takerBidUser.address); - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) - ); - - // Check balance of WETH is same as expected - assert.deepEqual(expectedBalanceInWETH, await weth.balanceOf(takerBidUser.address)); - }); - - it("Standard Order/ERC1155/ETH only - MakerAsk order is matched by TakerBid order", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC1155.address, - tokenId: constants.One, - price: parseEther("3"), - amount: constants.Two, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder = createTakerOrder({ - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: constants.One, - price: parseEther("3"), - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - const tx = await looksRareExchange - .connect(takerBidUser) - .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { - value: takerBidOrder.price, - }); - - await expect(tx) - .to.emit(looksRareExchange, "TakerBid") - .withArgs( - computeOrderHash(makerAskOrder), - makerAskOrder.nonce, - takerBidUser.address, - makerAskUser.address, - strategyStandardSaleForFixedPrice.address, - makerAskOrder.currency, - makerAskOrder.collection, - makerAskOrder.tokenId, - makerAskOrder.amount, - takerBidOrder.price - ); - - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) - ); - - // User 2 had minted 2 tokenId=1 so he has 4 - assert.equal((await mockERC1155.balanceOf(takerBidUser.address, "1")).toString(), "4"); - }); - - it("Standard Order/ERC721/WETH only - MakerBid order is matched by TakerAsk order", async () => { - const makerBidUser = accounts[2]; - const takerAskUser = accounts[1]; - - const makerBidOrder = await createMakerOrder({ - isOrderAsk: false, - signer: makerBidUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerBidUser, - verifyingContract: looksRareExchange.address, - }); - - const takerAskOrder = createTakerOrder({ - isOrderAsk: true, - taker: takerAskUser.address, - tokenId: constants.Zero, - price: makerBidOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - const tx = await looksRareExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); - await expect(tx) - .to.emit(looksRareExchange, "TakerAsk") - .withArgs( - computeOrderHash(makerBidOrder), - makerBidOrder.nonce, - takerAskUser.address, - makerBidUser.address, - strategyStandardSaleForFixedPrice.address, - makerBidOrder.currency, - makerBidOrder.collection, - takerAskOrder.tokenId, - makerBidOrder.amount, - makerBidOrder.price - ); - - assert.equal(await mockERC721.ownerOf("0"), makerBidUser.address); - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(makerBidUser.address, makerBidOrder.nonce) - ); - }); - - it("Standard Order/ERC1155/WETH only - MakerBid order is matched by TakerAsk order", async () => { - const makerBidUser = accounts[1]; - const takerAskUser = accounts[2]; - - const makerBidOrder = await createMakerOrder({ - isOrderAsk: false, - signer: makerBidUser.address, - collection: mockERC1155.address, - tokenId: BigNumber.from("3"), - price: parseEther("3"), - amount: constants.Two, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerBidUser, - verifyingContract: looksRareExchange.address, - }); - - const takerAskOrder = createTakerOrder({ - isOrderAsk: true, - taker: takerAskUser.address, - tokenId: BigNumber.from("3"), - price: makerBidOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - const tx = await looksRareExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); - await expect(tx) - .to.emit(looksRareExchange, "TakerAsk") - .withArgs( - computeOrderHash(makerBidOrder), - makerBidOrder.nonce, - takerAskUser.address, - makerBidUser.address, - strategyStandardSaleForFixedPrice.address, - makerBidOrder.currency, - makerBidOrder.collection, - takerAskOrder.tokenId, - makerBidOrder.amount, - makerBidOrder.price - ); - - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(makerBidUser.address, makerBidOrder.nonce) - ); - }); - }); - - describe("#2 - Non-standard orders", async () => { - it("ERC1271/Contract Signature - MakerBid order is matched by TakerAsk order", async () => { - const userSigningThroughContract = accounts[1]; - const takerAskUser = accounts[2]; - - const MockSignerContract = await ethers.getContractFactory("MockSignerContract"); - const mockSignerContract = await MockSignerContract.connect(userSigningThroughContract).deploy(); - await mockSignerContract.deployed(); - - await weth.connect(userSigningThroughContract).transfer(mockSignerContract.address, parseEther("1")); - await mockSignerContract - .connect(userSigningThroughContract) - .approveERC20ToBeSpent(weth.address, looksRareExchange.address); - - const makerBidOrder = await createMakerOrder({ - isOrderAsk: false, - signer: mockSignerContract.address, - collection: mockERC721.address, - tokenId: constants.One, - price: parseEther("1"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: userSigningThroughContract, - verifyingContract: looksRareExchange.address, - }); - - const takerAskOrder = createTakerOrder({ - isOrderAsk: true, - taker: takerAskUser.address, - tokenId: makerBidOrder.tokenId, - price: makerBidOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - const tx = await looksRareExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); - await expect(tx) - .to.emit(looksRareExchange, "TakerAsk") - .withArgs( - computeOrderHash(makerBidOrder), - makerBidOrder.nonce, - takerAskUser.address, - mockSignerContract.address, - strategyStandardSaleForFixedPrice.address, - makerBidOrder.currency, - makerBidOrder.collection, - takerAskOrder.tokenId, - makerBidOrder.amount, - makerBidOrder.price - ); - - // Verify funds/tokens were transferred - assert.equal(await mockERC721.ownerOf("1"), mockSignerContract.address); - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(mockSignerContract.address, makerBidOrder.nonce) - ); - - // Withdraw it back - await mockSignerContract.connect(userSigningThroughContract).withdrawERC721NFT(mockERC721.address, "1"); - assert.equal(await mockERC721.ownerOf("1"), userSigningThroughContract.address); - }); - - it("ERC1271/Contract Signature - MakerAsk order is matched by TakerBid order", async () => { - const userSigningThroughContract = accounts[1]; - const takerBidUser = accounts[2]; - const MockSignerContract = await ethers.getContractFactory("MockSignerContract"); - const mockSignerContract = await MockSignerContract.connect(userSigningThroughContract).deploy(); - await mockSignerContract.deployed(); - - await mockERC721 - .connect(userSigningThroughContract) - .transferFrom(userSigningThroughContract.address, mockSignerContract.address, "0"); - - await mockSignerContract - .connect(userSigningThroughContract) - .approveERC721NFT(mockERC721.address, transferManagerERC721.address); - - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: mockSignerContract.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("1"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: userSigningThroughContract, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder = createTakerOrder({ - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: makerAskOrder.tokenId, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - const tx = await looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); - await expect(tx) - .to.emit(looksRareExchange, "TakerBid") - .withArgs( - computeOrderHash(makerAskOrder), - makerAskOrder.nonce, - takerBidUser.address, - mockSignerContract.address, - strategyStandardSaleForFixedPrice.address, - makerAskOrder.currency, - makerAskOrder.collection, - takerBidOrder.tokenId, - makerAskOrder.amount, - makerAskOrder.price - ); - - // Verify funds/tokens were transferred - assert.equal(await mockERC721.ownerOf("1"), takerBidUser.address); - assert.deepEqual(await weth.balanceOf(mockSignerContract.address), takerBidOrder.price.mul("9800").div("10000")); - - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(mockSignerContract.address, makerAskOrder.nonce) - ); - - // Withdraw WETH back - await mockSignerContract.connect(userSigningThroughContract).withdrawERC20(weth.address); - assert.deepEqual(await weth.balanceOf(mockSignerContract.address), constants.Zero); - }); - }); - - describe("#3 - Royalty fee system", async () => { - it("Fee/Royalty - Payment with ERC2981 works for non-ETH orders", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - assert.equal(await mockERC721WithRoyalty.RECEIVER(), royaltyCollector.address); - assert.isTrue(await mockERC721WithRoyalty.supportsInterface("0x2a55205a")); - - // Verify balance of royaltyCollector is 0 - assert.deepEqual(await weth.balanceOf(royaltyCollector.address), constants.Zero); - - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721WithRoyalty.address, - price: parseEther("3"), - tokenId: constants.Zero, - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: takerBidUser.address, - price: makerAskOrder.price, - tokenId: makerAskOrder.tokenId, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - const tx = await looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); - await expect(tx) - .to.emit(looksRareExchange, "TakerBid") - .withArgs( - computeOrderHash(makerAskOrder), - makerAskOrder.nonce, - takerBidUser.address, - makerAskUser.address, - strategyStandardSaleForFixedPrice.address, - makerAskOrder.currency, - makerAskOrder.collection, - takerBidOrder.tokenId, - makerAskOrder.amount, - makerAskOrder.price - ); - - const expectedRoyaltyAmount = BigNumber.from(takerBidOrder.price).mul("200").div("10000"); - - await expect(tx) - .to.emit(looksRareExchange, "RoyaltyPayment") - .withArgs( - makerAskOrder.collection, - takerBidOrder.tokenId, - royaltyCollector.address, - makerAskOrder.currency, - expectedRoyaltyAmount - ); - - assert.equal(await mockERC721WithRoyalty.ownerOf("0"), takerBidUser.address); - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) - ); - - // Verify WETH balance of royalty collector has increased - assert.deepEqual(await weth.balanceOf(royaltyCollector.address), expectedRoyaltyAmount); - }); - - it("Fee/Royalty - Payment with ERC2981 works for ETH orders", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - assert.equal(await mockERC721WithRoyalty.RECEIVER(), royaltyCollector.address); - assert.isTrue(await mockERC721WithRoyalty.supportsInterface("0x2a55205a")); - - // Verify balance of royaltyCollector is 0 - assert.deepEqual(await weth.balanceOf(royaltyCollector.address), constants.Zero); - - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721WithRoyalty.address, - price: parseEther("3"), - tokenId: constants.Zero, - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: takerBidUser.address, - price: makerAskOrder.price, - tokenId: makerAskOrder.tokenId, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - const tx = await looksRareExchange - .connect(takerBidUser) - .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { - value: parseEther("3"), - }); - - await expect(tx) - .to.emit(looksRareExchange, "TakerBid") - .withArgs( - computeOrderHash(makerAskOrder), - makerAskOrder.nonce, - takerBidUser.address, - makerAskUser.address, - strategyStandardSaleForFixedPrice.address, - makerAskOrder.currency, - makerAskOrder.collection, - takerBidOrder.tokenId, - makerAskOrder.amount, - makerAskOrder.price - ); - - const expectedRoyaltyAmount = BigNumber.from(takerBidOrder.price).mul("200").div("10000"); - - await expect(tx) - .to.emit(looksRareExchange, "RoyaltyPayment") - .withArgs( - makerAskOrder.collection, - takerBidOrder.tokenId, - royaltyCollector.address, - makerAskOrder.currency, - expectedRoyaltyAmount - ); - assert.equal(await mockERC721WithRoyalty.ownerOf("0"), takerBidUser.address); - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) - ); - - // Verify WETH balance of royalty collector has increased - assert.deepEqual(await weth.balanceOf(royaltyCollector.address), expectedRoyaltyAmount); - }); - - it("Fee/Royalty - Payment for custom integration works", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - // Set 3% for royalties - const fee = "300"; - let tx = await royaltyFeeSetter - .connect(admin) - .updateRoyaltyInfoForCollection(mockERC721.address, admin.address, royaltyCollector.address, fee); - - await expect(tx) - .to.emit(royaltyFeeRegistry, "RoyaltyFeeUpdate") - .withArgs(mockERC721.address, admin.address, royaltyCollector.address, fee); - - tx = await royaltyFeeRegistry.royaltyFeeInfoCollection(mockERC721.address); - assert.equal(tx[0], admin.address); - assert.equal(tx[1], royaltyCollector.address); - assert.equal(tx[2].toString(), fee); - - // Verify balance of royaltyCollector is 0 - assert.deepEqual(await weth.balanceOf(royaltyCollector.address), constants.Zero); - - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - price: parseEther("3"), - tokenId: constants.Zero, - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: constants.Zero, - price: parseEther("3"), - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - tx = await looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); - - await expect(tx) - .to.emit(looksRareExchange, "TakerBid") - .withArgs( - computeOrderHash(makerAskOrder), - makerAskOrder.nonce, - takerBidUser.address, - makerAskUser.address, - strategyStandardSaleForFixedPrice.address, - makerAskOrder.currency, - makerAskOrder.collection, - takerBidOrder.tokenId, - makerAskOrder.amount, - makerAskOrder.price - ); - - const expectedRoyaltyAmount = BigNumber.from(takerBidOrder.price).mul(fee).div("10000"); - - await expect(tx) - .to.emit(looksRareExchange, "RoyaltyPayment") - .withArgs( - makerAskOrder.collection, - takerBidOrder.tokenId, - royaltyCollector.address, - makerAskOrder.currency, - expectedRoyaltyAmount - ); - - assert.equal(await mockERC721.ownerOf("0"), takerBidUser.address); - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) - ); - - // Verify WETH balance of royalty collector has increased - assert.deepEqual(await weth.balanceOf(royaltyCollector.address), expectedRoyaltyAmount); - }); - - it("Fee/Royalty - Slippage protection works for MakerAsk", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - // Set 3% for royalties - const fee = "300"; - await royaltyFeeSetter - .connect(admin) - .updateRoyaltyInfoForCollection(mockERC721.address, admin.address, royaltyCollector.address, fee); - - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - price: parseEther("3"), - tokenId: constants.Zero, - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: BigNumber.from("9500"), // ProtocolFee: 2%, RoyaltyFee: 3% - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - // Update to 3.01% for royalties - await royaltyFeeSetter - .connect(admin) - .updateRoyaltyInfoForCollection(mockERC721.address, admin.address, royaltyCollector.address, "301"); - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: constants.Zero, - price: parseEther("3"), - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { - value: parseEther("3"), - }) - ).to.be.revertedWith("Fees: Higher than expected"); - - // Update back to 3.00% for royalties - await royaltyFeeSetter - .connect(admin) - .updateRoyaltyInfoForCollection(mockERC721.address, admin.address, royaltyCollector.address, fee); - - // Trade is executed - const tx = await looksRareExchange - .connect(takerBidUser) - .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { - value: parseEther("3"), - }); - - await expect(tx) - .to.emit(looksRareExchange, "TakerBid") - .withArgs( - computeOrderHash(makerAskOrder), - makerAskOrder.nonce, - takerBidUser.address, - makerAskUser.address, - strategyStandardSaleForFixedPrice.address, - makerAskOrder.currency, - makerAskOrder.collection, - takerBidOrder.tokenId, - makerAskOrder.amount, - makerAskOrder.price - ); - }); - - it("Fee/Royalty - Slippage protection works for TakerAsk", async () => { - const makerBidUser = accounts[2]; - const takerAskUser = accounts[1]; - - const makerBidOrder = await createMakerOrder({ - isOrderAsk: false, - signer: makerBidUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerBidUser, - verifyingContract: looksRareExchange.address, - }); - - const takerAskOrder = createTakerOrder({ - isOrderAsk: true, - taker: takerAskUser.address, - tokenId: constants.Zero, - price: parseEther("3"), - minPercentageToAsk: BigNumber.from("9500"), // ProtocolFee: 2%, RoyaltyFee: 3% - params: defaultAbiCoder.encode([], []), - }); - - // Update to 3.01% for royalties - await royaltyFeeSetter - .connect(admin) - .updateRoyaltyInfoForCollection(mockERC721.address, admin.address, royaltyCollector.address, "301"); - - await expect( - looksRareExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder) - ).to.be.revertedWith("Fees: Higher than expected"); - - // Update back to 3.00% for royalties - await royaltyFeeSetter - .connect(admin) - .updateRoyaltyInfoForCollection(mockERC721.address, admin.address, royaltyCollector.address, "300"); - - const tx = await looksRareExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); - await expect(tx) - .to.emit(looksRareExchange, "TakerAsk") - .withArgs( - computeOrderHash(makerBidOrder), - makerBidOrder.nonce, - takerAskUser.address, - makerBidUser.address, - strategyStandardSaleForFixedPrice.address, - makerBidOrder.currency, - makerBidOrder.collection, - takerAskOrder.tokenId, - makerBidOrder.amount, - makerBidOrder.price - ); - }); - - it("Fee/Royalty/Private Sale - Royalty fee is collected but no platform fee", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - // Verify balance of royaltyCollector is 0 - assert.deepEqual(await weth.balanceOf(royaltyCollector.address), constants.Zero); - - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721WithRoyalty.address, - price: parseEther("3"), - tokenId: constants.Zero, - amount: constants.One, - strategy: strategyPrivateSale.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode(["address"], [takerBidUser.address]), // target user - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: constants.Zero, - price: parseEther("3"), - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - const tx = await looksRareExchange - .connect(takerBidUser) - .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { - value: parseEther("3"), - }); - - await expect(tx) - .to.emit(looksRareExchange, "TakerBid") - .withArgs( - computeOrderHash(makerAskOrder), - makerAskOrder.nonce, - takerBidUser.address, - makerAskUser.address, - strategyPrivateSale.address, - makerAskOrder.currency, - makerAskOrder.collection, - takerBidOrder.tokenId, - makerAskOrder.amount, - makerAskOrder.price - ); - - assert.equal(await mockERC721WithRoyalty.ownerOf("0"), takerBidUser.address); - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) - ); - - // Verify WETH balance of royalty collector has increased - assert.deepEqual(await weth.balanceOf(royaltyCollector.address), takerBidOrder.price.mul("200").div("10000")); - - // Verify balance of admin (aka treasury) is 0 - assert.deepEqual(await weth.balanceOf(admin.address), constants.Zero); - }); - - it("RoyaltyFeeSetter - Owner can set the royalty fee", async () => { - const fee = "200"; - const MockERC721WithOwner = await ethers.getContractFactory("MockERC721WithOwner"); - const mockERC721WithOwner = await MockERC721WithOwner.deploy("Mock Ownable ERC721", "MOERC721"); - await mockERC721WithOwner.deployed(); - - await expect( - royaltyFeeSetter - .connect(admin) - .updateRoyaltyInfoForCollectionIfAdmin( - mockERC721WithOwner.address, - royaltyCollector.address, - royaltyCollector.address, - fee - ) - ).to.be.revertedWith("function selector was not recognized and there's no fallback function"); - - const tx = await royaltyFeeSetter - .connect(admin) - .updateRoyaltyInfoForCollectionIfOwner( - mockERC721WithOwner.address, - royaltyCollector.address, - royaltyCollector.address, - fee - ); - - await expect(tx) - .to.emit(royaltyFeeRegistry, "RoyaltyFeeUpdate") - .withArgs(mockERC721WithOwner.address, royaltyCollector.address, royaltyCollector.address, fee); - }); - - it("RoyaltyFeeSetter - Admin can set the royalty fee", async () => { - const fee = "200"; - const MockERC721WithAdmin = await ethers.getContractFactory("MockERC721WithAdmin"); - const mockERC721WithAdmin = await MockERC721WithAdmin.deploy("Mock Ownable ERC721", "MOERC721"); - await mockERC721WithAdmin.deployed(); - - let res = await royaltyFeeSetter.checkForCollectionSetter(mockERC721WithAdmin.address); - assert.equal(res[0], admin.address); - assert.equal(res[1].toString(), "3"); - - await expect( - royaltyFeeSetter - .connect(admin) - .updateRoyaltyInfoForCollectionIfOwner( - mockERC721WithAdmin.address, - royaltyCollector.address, - royaltyCollector.address, - fee - ) - ).to.be.revertedWith("function selector was not recognized and there's no fallback function"); - - const tx = await royaltyFeeSetter - .connect(admin) - .updateRoyaltyInfoForCollectionIfAdmin( - mockERC721WithAdmin.address, - royaltyCollector.address, - royaltyCollector.address, - "200" - ); - - await expect(tx) - .to.emit(royaltyFeeRegistry, "RoyaltyFeeUpdate") - .withArgs(mockERC721WithAdmin.address, royaltyCollector.address, royaltyCollector.address, fee); - - res = await royaltyFeeSetter.checkForCollectionSetter(mockERC721WithAdmin.address); - assert.equal(res[0], royaltyCollector.address); - assert.equal(res[1].toString(), "0"); - }); - - it("RoyaltyFeeSetter - Owner cannot set the royalty fee if already set", async () => { - const MockERC721WithOwner = await ethers.getContractFactory("MockERC721WithOwner"); - const mockERC721WithOwner = await MockERC721WithOwner.deploy("Mock Ownable ERC721", "MOERC721"); - await mockERC721WithOwner.deployed(); - - let res = await royaltyFeeSetter.checkForCollectionSetter(mockERC721WithOwner.address); - assert.equal(res[0], admin.address); - assert.equal(res[1].toString(), "2"); - - await royaltyFeeSetter - .connect(admin) - .updateRoyaltyInfoForCollectionIfOwner( - mockERC721WithOwner.address, - royaltyCollector.address, - royaltyCollector.address, - "200" - ); - - await expect( - royaltyFeeSetter - .connect(admin) - .updateRoyaltyInfoForCollectionIfOwner( - mockERC721WithOwner.address, - royaltyCollector.address, - royaltyCollector.address, - "200" - ) - ).to.been.revertedWith("Setter: Already set"); - - const tx = await royaltyFeeSetter - .connect(royaltyCollector) - .updateRoyaltyInfoForCollectionIfSetter( - mockERC721WithOwner.address, - royaltyCollector.address, - royaltyCollector.address, - "200" - ); - - await expect(tx) - .to.emit(royaltyFeeRegistry, "RoyaltyFeeUpdate") - .withArgs(mockERC721WithOwner.address, royaltyCollector.address, royaltyCollector.address, "200"); - - res = await royaltyFeeSetter.checkForCollectionSetter(mockERC721WithOwner.address); - assert.equal(res[0], royaltyCollector.address); - assert.equal(res[1].toString(), "0"); - }); - - it("RoyaltyFeeSetter - No function selector if no admin()/owner() function", async () => { - const res = await royaltyFeeSetter.checkForCollectionSetter(mockERC721.address); - assert.equal(res[0], constants.AddressZero); - assert.equal(res[1].toString(), "4"); - - await expect( - royaltyFeeSetter - .connect(admin) - .updateRoyaltyInfoForCollectionIfOwner(mockERC721.address, admin.address, royaltyCollector.address, "200") - ).to.be.revertedWith("function selector was not recognized and there's no fallback function"); - - await expect( - royaltyFeeSetter.updateRoyaltyInfoForCollectionIfAdmin( - mockERC721.address, - admin.address, - royaltyCollector.address, - "200" - ) - ).to.be.revertedWith("function selector was not recognized and there's no fallback function"); - }); - - it("RoyaltyFeeSetter - Cannot adjust if not the setter", async () => { - await expect( - royaltyFeeSetter.updateRoyaltyInfoForCollectionIfSetter( - mockERC721.address, - admin.address, - royaltyCollector.address, - "200" - ) - ).to.be.revertedWith("Setter: Not the setter"); - }); - - it("RoyaltyFeeSetter - Cannot set a royalty fee too high", async () => { - await expect( - royaltyFeeSetter - .connect(admin) - .updateRoyaltyInfoForCollection( - mockERC721.address, - royaltyCollector.address, - royaltyCollector.address, - "9501" - ) - ).to.be.revertedWith("Registry: Royalty fee too high"); - }); - - it("RoyaltyFeeSetter - Cannot set a royalty fee if not compliant", async () => { - const MockNonCompliantERC721 = await ethers.getContractFactory("MockNonCompliantERC721"); - const mockNonCompliantERC721 = await MockNonCompliantERC721.deploy("Mock Bad ERC721", "MBERC721"); - await mockNonCompliantERC721.deployed(); - - await expect( - royaltyFeeSetter - .connect(admin) - .updateRoyaltyInfoForCollectionIfOwner( - mockNonCompliantERC721.address, - royaltyCollector.address, - royaltyCollector.address, - "500" - ) - ).to.be.revertedWith("Setter: Not ERC721/ERC1155"); - }); - - it("RoyaltyFeeSetter - Cannot set custom royalty fee if ERC2981", async () => { - const res = await royaltyFeeSetter.checkForCollectionSetter(mockERC721WithRoyalty.address); - - assert.equal(res[0], constants.AddressZero); - assert.equal(res[1].toString(), "1"); - - await expect( - royaltyFeeSetter - .connect(admin) - .updateRoyaltyInfoForCollectionIfOwner( - mockERC721WithRoyalty.address, - royaltyCollector.address, - royaltyCollector.address, - "500" - ) - ).to.be.revertedWith("Owner: Must not be ERC2981"); - - await expect( - royaltyFeeSetter - .connect(admin) - .updateRoyaltyInfoForCollectionIfAdmin( - mockERC721WithRoyalty.address, - royaltyCollector.address, - royaltyCollector.address, - "500" - ) - ).to.be.revertedWith("Admin: Must not be ERC2981"); - }); - }); - - describe("#4 - Standard logic revertions", async () => { - it("One Cancel Other - Initial order is not executable anymore", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - const initialMakerAskOrder: MakerOrderWithSignature = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - price: parseEther("3"), - tokenId: constants.Zero, - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const adjustedMakerAskOrder: MakerOrderWithSignature = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - price: parseEther("2.5"), - tokenId: constants.Zero, - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder = createTakerOrder({ - isOrderAsk: false, - taker: takerBidUser.address, - price: parseEther("2.5"), - tokenId: constants.Zero, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - const tx = await looksRareExchange - .connect(takerBidUser) - .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, adjustedMakerAskOrder, { - value: takerBidOrder.price, - }); - - await expect(tx) - .to.emit(looksRareExchange, "TakerBid") - .withArgs( - computeOrderHash(adjustedMakerAskOrder), - adjustedMakerAskOrder.nonce, - takerBidUser.address, - makerAskUser.address, - strategyStandardSaleForFixedPrice.address, - adjustedMakerAskOrder.currency, - adjustedMakerAskOrder.collection, - takerBidOrder.tokenId, - adjustedMakerAskOrder.amount, - adjustedMakerAskOrder.price - ); - - assert.equal(await mockERC721.ownerOf("0"), takerBidUser.address); - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, adjustedMakerAskOrder.nonce) - ); - - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, initialMakerAskOrder.nonce) - ); - - // Initial order is not executable anymore - await expect( - looksRareExchange - .connect(takerBidUser) - .matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, initialMakerAskOrder, { - value: takerBidOrder.price, - }) - ).to.be.revertedWith("Order: Matching order expired"); - }); - - it("Cancel - Cannot match if order was cancelled", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder = createTakerOrder({ - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: makerAskOrder.tokenId, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - const tx = await looksRareExchange.connect(makerAskUser).cancelMultipleMakerOrders([makerAskOrder.nonce]); - // Event params are not tested because of array issue with BN - await expect(tx).to.emit(looksRareExchange, "CancelMultipleOrders"); - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { - value: takerBidOrder.price, - }) - ).to.be.revertedWith("Order: Matching order expired"); - }); - - it("Cancel - Cannot match if on a different checkpoint than current on-chain signer's checkpoint", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[3]; - - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - price: parseEther("3"), - tokenId: constants.Zero, - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder = createTakerOrder({ - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: makerAskOrder.tokenId, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - const tx = await looksRareExchange.connect(makerAskUser).cancelAllOrdersForSender("1"); - await expect(tx).to.emit(looksRareExchange, "CancelAllOrders").withArgs(makerAskUser.address, "1"); - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { - value: takerBidOrder.price, - }) - ).to.be.revertedWith("Order: Matching order expired"); - }); - - it("Order - Cannot match if msg.value is too high", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[3]; - - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: takerBidUser.address, - price: makerAskOrder.price, - tokenId: makerAskOrder.tokenId, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { - value: takerBidOrder.price.add(constants.One), - }) - ).to.be.revertedWith("Order: Msg.value too high"); - }); - - it("Order - Cannot match is amount is 0", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[3]; - - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.Zero, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: makerAskOrder.tokenId, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder, {}) - ).to.be.revertedWith("Order: Amount cannot be 0"); - }); - - it("Order - Cannot match 2 ask orders, 2 bid orders, or taker not the sender", async () => { - const makerAskUser = accounts[2]; - const fakeTakerUser = accounts[3]; - const takerBidUser = accounts[4]; - - // 1. MATCH ASK WITH TAKER BID - // 1.1 Signer is not the actual signer - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: makerAskOrder.tokenId, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - await expect( - looksRareExchange.connect(fakeTakerUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder, {}) - ).to.be.revertedWith("Order: Taker must be the sender"); - - await expect( - looksRareExchange.connect(fakeTakerUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { - value: takerBidOrder.price, - }) - ).to.be.revertedWith("Order: Taker must be the sender"); - - // 1.2 Wrong sides - takerBidOrder.isOrderAsk = true; - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder, {}) - ).to.be.revertedWith("Order: Wrong sides"); - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { - value: takerBidOrder.price, - }) - ).to.be.revertedWith("Order: Wrong sides"); - - makerAskOrder.isOrderAsk = false; - - // No need to duplicate tests again - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder, {}) - ).to.be.revertedWith("Order: Wrong sides"); - - takerBidOrder.isOrderAsk = false; - - // No need to duplicate tests again - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) - ).to.be.revertedWith("Order: Wrong sides"); - - // 2. MATCH ASK WITH TAKER BID - // 2.1 Signer is not the actual signer - const takerAskUser = accounts[1]; - const makerBidUser = accounts[2]; - - const makerBidOrder = await createMakerOrder({ - isOrderAsk: false, - signer: makerBidUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerBidUser, - verifyingContract: looksRareExchange.address, - }); - - const takerAskOrder = createTakerOrder({ - isOrderAsk: true, - taker: takerAskUser.address, - tokenId: makerAskOrder.tokenId, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - await expect( - looksRareExchange.connect(fakeTakerUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder) - ).to.be.revertedWith("Order: Taker must be the sender"); - - // 2.2 Wrong sides - takerAskOrder.isOrderAsk = false; - - await expect( - looksRareExchange.connect(makerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder) - ).to.be.revertedWith("Order: Wrong sides"); - - makerBidOrder.isOrderAsk = true; - - await expect( - looksRareExchange.connect(takerBidUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder) - ).to.be.revertedWith("Order: Wrong sides"); - - takerAskOrder.isOrderAsk = true; - - await expect( - looksRareExchange.connect(takerBidUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder, {}) - ).to.be.revertedWith("Order: Wrong sides"); - }); - - it("Cancel - Cannot cancel all at an nonce equal or lower than existing one", async () => { - await expect(looksRareExchange.connect(accounts[1]).cancelAllOrdersForSender("0")).to.be.revertedWith( - "Cancel: Order nonce lower than current" - ); - - await expect(looksRareExchange.connect(accounts[1]).cancelAllOrdersForSender("500000")).to.be.revertedWith( - "Cancel: Cannot cancel more orders" - ); - - // Change the minimum nonce for user to 2 - await looksRareExchange.connect(accounts[1]).cancelAllOrdersForSender("2"); - - await expect(looksRareExchange.connect(accounts[1]).cancelAllOrdersForSender("1")).to.be.revertedWith( - "Cancel: Order nonce lower than current" - ); - - await expect(looksRareExchange.connect(accounts[1]).cancelAllOrdersForSender("2")).to.be.revertedWith( - "Cancel: Order nonce lower than current" - ); - }); - - it("Cancel - Cannot cancel all at an nonce equal than existing one", async () => { - // Change the minimum nonce for user to 2 - await looksRareExchange.connect(accounts[1]).cancelAllOrdersForSender("2"); - - await expect(looksRareExchange.connect(accounts[1]).cancelMultipleMakerOrders(["0"])).to.be.revertedWith( - "Cancel: Order nonce lower than current" - ); - - await expect(looksRareExchange.connect(accounts[1]).cancelMultipleMakerOrders(["3", "1"])).to.be.revertedWith( - "Cancel: Order nonce lower than current" - ); - - // Can cancel at the same nonce that minimum one - await looksRareExchange.connect(accounts[1]).cancelMultipleMakerOrders(["2"]); - }); - - it("Order - Cannot trade before startTime", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - startTimeOrder = BigNumber.from( - (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp - ).add("5000"); - endTimeOrder = startTimeOrder.add(BigNumber.from("10000")); - - const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder = createTakerOrder({ - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: makerAskOrder.tokenId, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder) - ).to.be.revertedWith("Strategy: Execution invalid"); - - await increaseTo(startTimeOrder); - await looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); - }); - - it("Order - Cannot trade after endTime", async () => { - const makerBidUser = accounts[2]; - const takerAskUser = accounts[1]; - - endTimeOrder = startTimeOrder.add(BigNumber.from("5000")); - - const makerBidOrder: MakerOrderWithSignature = await createMakerOrder({ - isOrderAsk: false, - signer: makerBidUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerBidUser, - verifyingContract: looksRareExchange.address, - }); - - const takerAskOrder = createTakerOrder({ - isOrderAsk: true, - taker: takerAskUser.address, - tokenId: makerBidOrder.tokenId, - price: makerBidOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - await increaseTo(endTimeOrder.add(1)); - - await expect( - looksRareExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder) - ).to.be.revertedWith("Strategy: Execution invalid"); - }); - - it("Currency - Cannot match if currency is removed", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - const tx = await currencyManager.connect(admin).removeCurrency(weth.address); - await expect(tx).to.emit(currencyManager, "CurrencyRemoved").withArgs(weth.address); - - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: makerAskOrder.tokenId, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { - value: takerBidOrder.price, - }) - ).to.be.revertedWith("Currency: Not whitelisted"); - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder, {}) - ).to.be.revertedWith("Currency: Not whitelisted"); - }); - - it("Currency - Cannot use function to match MakerAsk with native asset if maker currency not WETH", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: mockUSDT.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: makerAskOrder.price, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { - value: takerBidOrder.price, - }) - ).to.be.revertedWith("Order: Currency must be WETH"); - }); - - it("Currency - Cannot match until currency is whitelisted", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - // Each users mints 1M USDT - await mockUSDT.connect(takerBidUser).mint(takerBidUser.address, parseEther("1000000")); - - // Set approval for USDT - await mockUSDT.connect(takerBidUser).approve(looksRareExchange.address, constants.MaxUint256); - - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: mockUSDT.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: makerAskOrder.tokenId, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder, {}) - ).to.be.revertedWith("Currency: Not whitelisted"); - - let tx = await currencyManager.connect(admin).addCurrency(mockUSDT.address); - await expect(tx).to.emit(currencyManager, "CurrencyWhitelisted").withArgs(mockUSDT.address); - - tx = await looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); - await expect(tx) - .to.emit(looksRareExchange, "TakerBid") - .withArgs( - computeOrderHash(makerAskOrder), - makerAskOrder.nonce, - takerBidUser.address, - makerAskUser.address, - strategyStandardSaleForFixedPrice.address, - makerAskOrder.currency, - makerAskOrder.collection, - takerBidOrder.tokenId, - makerAskOrder.amount, - makerAskOrder.price - ); - }); - - it("Strategy - Cannot match if strategy not whitelisted", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: makerAskOrder.tokenId, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - let tx = await executionManager.connect(admin).removeStrategy(strategyStandardSaleForFixedPrice.address); - await expect(tx).to.emit(executionManager, "StrategyRemoved").withArgs(strategyStandardSaleForFixedPrice.address); - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) - ).to.be.revertedWith("Strategy: Not whitelisted"); - - tx = await executionManager.connect(admin).addStrategy(strategyStandardSaleForFixedPrice.address); - await expect(tx) - .to.emit(executionManager, "StrategyWhitelisted") - .withArgs(strategyStandardSaleForFixedPrice.address); - - tx = await looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); - - await expect(tx) - .to.emit(looksRareExchange, "TakerBid") - .withArgs( - computeOrderHash(makerAskOrder), - makerAskOrder.nonce, - takerBidUser.address, - makerAskUser.address, - strategyStandardSaleForFixedPrice.address, - makerAskOrder.currency, - makerAskOrder.collection, - takerBidOrder.tokenId, - makerAskOrder.amount, - makerAskOrder.price - ); - }); - - it("Transfer - Cannot match if no transfer manager", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - const MockNonCompliantERC721 = await ethers.getContractFactory("MockNonCompliantERC721"); - const mockNonCompliantERC721 = await MockNonCompliantERC721.deploy("Mock Bad ERC721", "MBERC721"); - await mockNonCompliantERC721.deployed(); - - // User1 mints tokenId=0 - await mockNonCompliantERC721.connect(makerAskUser).mint(makerAskUser.address); - - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockNonCompliantERC721.address, - price: parseEther("3"), - tokenId: constants.Zero, - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: makerAskOrder.tokenId, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder, {}) - ).to.be.revertedWith("Transfer: No NFT transfer manager available"); - - let tx = await transferSelectorNFT - .connect(admin) - .addCollectionTransferManager(mockNonCompliantERC721.address, transferManagerNonCompliantERC721.address); - - await expect(tx) - .to.emit(transferSelectorNFT, "CollectionTransferManagerAdded") - .withArgs(mockNonCompliantERC721.address, transferManagerNonCompliantERC721.address); - - assert.equal( - await transferSelectorNFT.transferManagerSelectorForCollection(mockNonCompliantERC721.address), - transferManagerNonCompliantERC721.address - ); - - // User approves custom transfer manager contract - await mockNonCompliantERC721 - .connect(makerAskUser) - .setApprovalForAll(transferManagerNonCompliantERC721.address, true); - - tx = await looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); - - await expect(tx) - .to.emit(looksRareExchange, "TakerBid") - .withArgs( - computeOrderHash(makerAskOrder), - makerAskOrder.nonce, - takerBidUser.address, - makerAskUser.address, - strategyStandardSaleForFixedPrice.address, - makerAskOrder.currency, - makerAskOrder.collection, - takerBidOrder.tokenId, - makerAskOrder.amount, - makerAskOrder.price - ); - - tx = await transferSelectorNFT.removeCollectionTransferManager(mockNonCompliantERC721.address); - - await expect(tx) - .to.emit(transferSelectorNFT, "CollectionTransferManagerRemoved") - .withArgs(mockNonCompliantERC721.address); - - assert.equal( - await transferSelectorNFT.transferManagerSelectorForCollection(mockNonCompliantERC721.address), - constants.AddressZero - ); - }); - }); - - describe("#5 - Unusual logic revertions", async () => { - it("CurrencyManager/ExecutionManager - Revertions work as expected", async () => { - await expect(currencyManager.connect(admin).addCurrency(weth.address)).to.be.revertedWith( - "Currency: Already whitelisted" - ); - - await expect(currencyManager.connect(admin).removeCurrency(mockUSDT.address)).to.be.revertedWith( - "Currency: Not whitelisted" - ); - - await expect(executionManager.connect(admin).addStrategy(strategyPrivateSale.address)).to.be.revertedWith( - "Strategy: Already whitelisted" - ); - - // MockUSDT is obviously not a strategy but this checks only if the address is in enumerable set - await expect(executionManager.connect(admin).removeStrategy(mockUSDT.address)).to.be.revertedWith( - "Strategy: Not whitelisted" - ); - }); - - it("SignatureChecker - Cannot match if v parameters is not 27 or 28", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - makerAskOrder.v = 29; - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: makerAskOrder.tokenId, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) - ).to.be.revertedWith("Signature: Invalid v parameter"); - }); - - it("SignatureChecker - Cannot match if invalid s parameter", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - // The s value is picked randomly to make the condition be rejected - makerAskOrder.s = "0x9ca0e65dda4b504989e1db8fc30095f24489ee7226465e9545c32fc7853fe985"; - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: takerBidUser.address, - price: makerAskOrder.price, - tokenId: makerAskOrder.tokenId, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) - ).to.be.revertedWith("Signature: Invalid s parameter"); - }); - - it("Order - Cannot cancel if no order", async () => { - await expect(looksRareExchange.connect(accounts[1]).cancelMultipleMakerOrders([])).to.be.revertedWith( - "Cancel: Cannot be empty" - ); - - await expect(looksRareExchange.connect(accounts[2]).cancelMultipleMakerOrders([])).to.be.revertedWith( - "Cancel: Cannot be empty" - ); - }); - - it("Order - Cannot execute if signer is null address", async () => { - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: constants.AddressZero, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: accounts[3], - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: accounts[2].address, - tokenId: makerAskOrder.tokenId, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - await expect( - looksRareExchange.connect(accounts[2]).matchAskWithTakerBid(takerBidOrder, makerAskOrder) - ).to.be.revertedWith("Order: Invalid signer"); - }); - - it("Order - Cannot execute if wrong signer", async () => { - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: accounts[1].address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyStandardSaleForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: accounts[3], - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: accounts[2].address, - tokenId: makerAskOrder.tokenId, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - await expect( - looksRareExchange.connect(accounts[2]).matchAskWithTakerBid(takerBidOrder, makerAskOrder) - ).to.be.revertedWith("Signature: Invalid"); - }); - - it("Transfer Managers - Transfer functions only callable by LooksRareExchange", async () => { - await expect( - transferManagerERC721 - .connect(accounts[5]) - .transferNonFungibleToken(mockERC721.address, accounts[1].address, accounts[5].address, "0", "1") - ).to.be.revertedWith("Transfer: Only LooksRare Exchange"); - - await expect( - transferManagerERC1155 - .connect(accounts[5]) - .transferNonFungibleToken(mockERC1155.address, accounts[1].address, accounts[5].address, "0", "2") - ).to.be.revertedWith("Transfer: Only LooksRare Exchange"); - - await expect( - transferManagerNonCompliantERC721 - .connect(accounts[5]) - .transferNonFungibleToken(mockERC721.address, accounts[1].address, accounts[5].address, "0", "1") - ).to.be.revertedWith("Transfer: Only LooksRare Exchange"); - }); - }); - - describe("#6 - Owner functions and access rights", async () => { - it("LooksRareExchange - Null address in owner functions", async () => { - await expect(looksRareExchange.connect(admin).updateCurrencyManager(constants.AddressZero)).to.be.revertedWith( - "Owner: Cannot be null address" - ); - - await expect(looksRareExchange.connect(admin).updateExecutionManager(constants.AddressZero)).to.be.revertedWith( - "Owner: Cannot be null address" - ); - - await expect(looksRareExchange.connect(admin).updateRoyaltyFeeManager(constants.AddressZero)).to.be.revertedWith( - "Owner: Cannot be null address" - ); - - await expect( - looksRareExchange.connect(admin).updateTransferSelectorNFT(constants.AddressZero) - ).to.be.revertedWith("Owner: Cannot be null address"); - }); - - it("LooksRareExchange - Owner functions work as expected", async () => { - let tx = await looksRareExchange.connect(admin).updateCurrencyManager(currencyManager.address); - await expect(tx).to.emit(looksRareExchange, "NewCurrencyManager").withArgs(currencyManager.address); - - tx = await looksRareExchange.connect(admin).updateExecutionManager(executionManager.address); - await expect(tx).to.emit(looksRareExchange, "NewExecutionManager").withArgs(executionManager.address); - - tx = await looksRareExchange.connect(admin).updateRoyaltyFeeManager(royaltyFeeManager.address); - await expect(tx).to.emit(looksRareExchange, "NewRoyaltyFeeManager").withArgs(royaltyFeeManager.address); - - tx = await looksRareExchange.connect(admin).updateProtocolFeeRecipient(admin.address); - await expect(tx).to.emit(looksRareExchange, "NewProtocolFeeRecipient").withArgs(admin.address); - }); - - it("TransferSelector - Owner revertions work as expected", async () => { - await expect( - transferSelectorNFT.connect(admin).addCollectionTransferManager(mockERC721.address, constants.AddressZero) - ).to.be.revertedWith("Owner: TransferManager cannot be null address"); - - await expect( - transferSelectorNFT - .connect(admin) - .addCollectionTransferManager(constants.AddressZero, transferManagerERC721.address) - ).to.be.revertedWith("Owner: Collection cannot be null address"); - - await expect( - transferSelectorNFT.connect(admin).removeCollectionTransferManager(mockERC721.address) - ).to.be.revertedWith("Owner: Collection has no transfer manager"); - }); - - it("FeeSetter/FeeRegistry - Owner functions work as expected", async () => { - let tx = await royaltyFeeSetter.connect(admin).updateRoyaltyFeeLimit("30"); - await expect(tx).to.emit(royaltyFeeRegistry, "NewRoyaltyFeeLimit").withArgs("30"); - - await expect(royaltyFeeSetter.connect(admin).updateRoyaltyFeeLimit("9501")).to.be.revertedWith( - "Owner: Royalty fee limit too high" - ); - - tx = await royaltyFeeSetter.connect(admin).updateOwnerOfRoyaltyFeeRegistry(admin.address); - await expect(tx) - .to.emit(royaltyFeeRegistry, "OwnershipTransferred") - .withArgs(royaltyFeeSetter.address, admin.address); - }); - - it("LooksRareExchange - Owner functions are only callable by owner", async () => { - const notAdminUser = accounts[3]; - - await expect( - looksRareExchange.connect(notAdminUser).updateCurrencyManager(currencyManager.address) - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect( - looksRareExchange.connect(notAdminUser).updateExecutionManager(executionManager.address) - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect( - looksRareExchange.connect(notAdminUser).updateProtocolFeeRecipient(notAdminUser.address) - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect( - looksRareExchange.connect(notAdminUser).updateRoyaltyFeeManager(royaltyFeeManager.address) - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect( - looksRareExchange.connect(notAdminUser).updateTransferSelectorNFT(transferSelectorNFT.address) - ).to.be.revertedWith("Ownable: caller is not the owner"); - }); - - it("CurrencyManager/ExecutionManager/RoyaltyFeeRegistry/RoyaltyFeeSetter/TransferSelectorNFT - Owner functions are only callable by owner", async () => { - const notAdminUser = accounts[3]; - - await expect(currencyManager.connect(notAdminUser).addCurrency(mockUSDT.address)).to.be.revertedWith( - "Ownable: caller is not the owner" - ); - - await expect(currencyManager.connect(notAdminUser).removeCurrency(weth.address)).to.be.revertedWith( - "Ownable: caller is not the owner" - ); - - await expect(executionManager.connect(notAdminUser).addStrategy(strategyPrivateSale.address)).to.be.revertedWith( - "Ownable: caller is not the owner" - ); - - await expect( - executionManager.connect(notAdminUser).removeStrategy(strategyPrivateSale.address) - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect(royaltyFeeRegistry.connect(notAdminUser).updateRoyaltyFeeLimit("30")).to.be.revertedWith( - "Ownable: caller is not the owner" - ); - - await expect( - royaltyFeeSetter - .connect(notAdminUser) - .updateRoyaltyInfoForCollection(mockERC721.address, notAdminUser.address, notAdminUser.address, "5000") - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect( - royaltyFeeSetter.connect(notAdminUser).updateOwnerOfRoyaltyFeeRegistry(notAdminUser.address) - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect(royaltyFeeSetter.connect(notAdminUser).updateRoyaltyFeeLimit("10")).to.be.revertedWith( - "Ownable: caller is not the owner" - ); - - await expect( - transferSelectorNFT - .connect(notAdminUser) - .addCollectionTransferManager(mockERC721WithRoyalty.address, transferManagerERC721.address) - ).to.be.revertedWith("Ownable: caller is not the owner"); - - await expect( - transferSelectorNFT.connect(notAdminUser).removeCollectionTransferManager(mockERC721WithRoyalty.address) - ).to.be.revertedWith("Ownable: caller is not the owner"); - }); - }); - - describe("#7 - View functions", async () => { - it("CurrencyManager - View functions work as expected", async () => { - // Add a 2nd currency - await currencyManager.connect(admin).addCurrency(mockUSDT.address); - - const numberCurrencies = await currencyManager.viewCountWhitelistedCurrencies(); - assert.equal(numberCurrencies.toString(), "2"); - - let tx = await currencyManager.viewWhitelistedCurrencies("0", "1"); - assert.equal(tx[0].length, 1); - assert.deepEqual(BigNumber.from(tx[1].toString()), constants.One); - - tx = await currencyManager.viewWhitelistedCurrencies("1", "100"); - assert.equal(tx[0].length, 1); - assert.deepEqual(BigNumber.from(tx[1].toString()), BigNumber.from(numberCurrencies.toString())); - }); - - it("ExecutionManager - View functions work as expected", async () => { - const numberStrategies = await executionManager.viewCountWhitelistedStrategies(); - assert.equal(numberStrategies.toString(), "5"); - - let tx = await executionManager.viewWhitelistedStrategies("0", "2"); - assert.equal(tx[0].length, 2); - assert.deepEqual(BigNumber.from(tx[1].toString()), constants.Two); - - tx = await executionManager.viewWhitelistedStrategies("2", "100"); - assert.equal(tx[0].length, 3); - assert.deepEqual(BigNumber.from(tx[1].toString()), BigNumber.from(numberStrategies.toString())); - }); - }); -}); diff --git a/test/strategies/strategyAnyItemFromCollectionForFixedPrice.test.ts b/test/strategies/strategyAnyItemFromCollectionForFixedPrice.test.ts new file mode 100644 index 0000000..fcb3ec4 --- /dev/null +++ b/test/strategies/strategyAnyItemFromCollectionForFixedPrice.test.ts @@ -0,0 +1,184 @@ +import { assert, expect } from "chai"; +import { BigNumber, constants, utils } from "ethers"; +import { ethers } from "hardhat"; + +import { MakerOrderWithSignature, TakerOrder } from "../helpers/order-types"; +import { createMakerOrder, createTakerOrder } from "../helpers/order-helper"; +import { computeOrderHash } from "../helpers/signature-helper"; +import { Contracts, Env, makeSuite, Snapshots } from "../_setup"; + +const { defaultAbiCoder, parseEther } = utils; + +makeSuite( + "Strategy - AnyItemFromCollectionForFixedPrice ('Collection orders')", + (contracts: Contracts, env: Env, snapshots: Snapshots) => { + // Other global variables + let startTimeOrder: BigNumber; + let endTimeOrder: BigNumber; + const emptyEncodedBytes = defaultAbiCoder.encode([], []); + + beforeEach(async () => { + // Set up defaults startTime/endTime (for orders) + startTimeOrder = BigNumber.from( + (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + ); + endTimeOrder = startTimeOrder.add(BigNumber.from("1000")); + }); + + afterEach(async () => { + await snapshots.revert("setup"); + }); + + it("ERC721 - MakerBid order is matched by TakerAsk order", async () => { + const makerBidUser = env.accounts[1]; + const takerAskUser = env.accounts[5]; + + const makerBidOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: false, + maker: makerBidUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, // Not used + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyAnyItemFromCollectionForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerBidUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerAskOrder = createTakerOrder({ + isOrderAsk: true, + taker: takerAskUser.address, + tokenId: BigNumber.from("4"), + price: makerBidOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const tx = await contracts.bendExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerAsk") + .withArgs( + computeOrderHash(makerBidOrder), + makerBidOrder.nonce, + takerAskUser.address, + makerBidUser.address, + contracts.strategyAnyItemFromCollectionForFixedPrice.address, + makerBidOrder.currency, + makerBidOrder.collection, + takerAskOrder.tokenId, + makerBidOrder.amount, + makerBidOrder.price + ); + + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerBidUser.address, makerBidOrder.nonce) + ); + }); + + it("ERC1155 - MakerBid order is matched by TakerAsk order", async () => { + const makerBidUser = env.accounts[1]; + const takerAskUser = env.accounts[2]; + + const makerBidOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: false, + maker: makerBidUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC1155.address, + tokenId: constants.Zero, // not used + price: parseEther("3"), + amount: constants.Two, + strategy: contracts.strategyAnyItemFromCollectionForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerBidUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerAskOrder = createTakerOrder({ + isOrderAsk: true, + taker: takerAskUser.address, + price: makerBidOrder.price, + tokenId: BigNumber.from("2"), + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const tx = await contracts.bendExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerAsk") + .withArgs( + computeOrderHash(makerBidOrder), + makerBidOrder.nonce, + takerAskUser.address, + makerBidUser.address, + contracts.strategyAnyItemFromCollectionForFixedPrice.address, + makerBidOrder.currency, + makerBidOrder.collection, + takerAskOrder.tokenId, + makerBidOrder.amount, + makerBidOrder.price + ); + + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerBidUser.address, makerBidOrder.nonce) + ); + }); + + it("Cannot match if wrong side", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyAnyItemFromCollectionForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) + ).to.be.revertedWith("Strategy: execution invalid"); + }); + } +); diff --git a/test/strategies/strategyAnyItemInASetForAFixedPrice.test.ts b/test/strategies/strategyAnyItemInASetForAFixedPrice.test.ts new file mode 100644 index 0000000..d150cec --- /dev/null +++ b/test/strategies/strategyAnyItemInASetForAFixedPrice.test.ts @@ -0,0 +1,213 @@ +import { assert, expect } from "chai"; +import { BigNumber, constants, utils } from "ethers"; +import { ethers } from "hardhat"; +import { MerkleTree } from "merkletreejs"; +/* eslint-disable node/no-extraneous-import */ +import { keccak256 } from "js-sha3"; + +import { MakerOrderWithSignature } from "../helpers/order-types"; +import { createMakerOrder, createTakerOrder } from "../helpers/order-helper"; +import { computeOrderHash } from "../helpers/signature-helper"; +import { Contracts, Env, makeSuite, Snapshots } from "../_setup"; + +const { defaultAbiCoder, parseEther } = utils; + +makeSuite( + "Strategy - AnyItemInASetForFixedPrice ('Trait orders')", + (contracts: Contracts, env: Env, snapshots: Snapshots) => { + let startTimeOrder: BigNumber; + let endTimeOrder: BigNumber; + const emptyEncodedBytes = defaultAbiCoder.encode([], []); + + beforeEach(async () => { + // Set up defaults startTime/endTime (for orders) + startTimeOrder = BigNumber.from( + (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + ); + endTimeOrder = startTimeOrder.add(BigNumber.from("1000")); + }); + + afterEach(async () => { + await snapshots.revert("setup"); + }); + + it("ERC721 - MakerAsk order is matched by TakerBid order", async () => { + const takerAskUser = env.accounts[3]; // has tokenId=2 + const makerBidUser = env.accounts[1]; + + // User wishes to buy either tokenId = 0, 2, 3, or 12 + const eligibleTokenIds = ["0", "2", "3", "12"]; + + // Compute the leaves using Solidity keccak256 (Equivalent of keccak256 with abi.encodePacked) and converts to hex + const leaves = eligibleTokenIds.map((x) => "0x" + utils.solidityKeccak256(["uint256"], [x]).substr(2)); + + // Compute MerkleTree based on the computed leaves + const tree = new MerkleTree(leaves, keccak256, { sortPairs: true }); + + // Compute the proof for index=1 (aka tokenId=2) + const hexProof = tree.getHexProof(leaves[1], 1); + + // Compute the root of the tree + const hexRoot = tree.getHexRoot(); + + // Verify leaf is matched in the tree with the computed root + assert.isTrue(tree.verify(hexProof, leaves[1], hexRoot)); + + const makerBidOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: false, + maker: makerBidUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyAnyItemInASetForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: defaultAbiCoder.encode(["bytes32"], [hexRoot]), + signerUser: makerBidUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerAskOrder = createTakerOrder({ + isOrderAsk: true, + taker: takerAskUser.address, + tokenId: BigNumber.from("2"), + price: makerBidOrder.price, + minPercentageToAsk: constants.Zero, + params: defaultAbiCoder.encode(["bytes32[]"], [hexProof]), + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + const tx = await contracts.bendExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerAsk") + .withArgs( + computeOrderHash(makerBidOrder), + makerBidOrder.nonce, + takerAskUser.address, + makerBidUser.address, + contracts.strategyAnyItemInASetForFixedPrice.address, + makerBidOrder.currency, + makerBidOrder.collection, + takerAskOrder.tokenId, + makerBidOrder.amount, + makerBidOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("2"), makerBidUser.address); + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerBidUser.address, makerBidOrder.nonce) + ); + }); + + it("ERC721 - TokenIds not in the set cannot be sold", async () => { + const takerAskUser = env.accounts[3]; // has tokenId=2 + const makerBidUser = env.accounts[1]; + + // User wishes to buy either tokenId = 1, 2, 3, 4, or 12 + const eligibleTokenIds = ["1", "2", "3", "4", "12"]; + + // Compute the leaves using Solidity keccak256 (Equivalent of keccak256 with abi.encodePacked) and converts to hex + const leaves = eligibleTokenIds.map((x) => "0x" + utils.solidityKeccak256(["uint256"], [x]).substr(2)); + + // Compute MerkleTree based on the computed leaves + const tree = new MerkleTree(leaves, keccak256, { sortPairs: true }); + + // Compute the proof for index=1 (aka tokenId=2) + const hexProof = tree.getHexProof(leaves[1], 1); + + // Compute the root of the tree + const hexRoot = tree.getHexRoot(); + + // Verify leaf is matched in the tree with the computed root + assert.isTrue(tree.verify(hexProof, leaves[1], hexRoot)); + + const makerBidOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: false, + maker: makerBidUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, // not used + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyAnyItemInASetForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: defaultAbiCoder.encode(["bytes32"], [hexRoot]), + signerUser: makerBidUser, + verifyingContract: contracts.bendExchange.address, + }); + + for (const tokenId of Array.from(Array(9).keys())) { + // If the tokenId is not included, it skips + if (!eligibleTokenIds.includes(tokenId.toString())) { + const takerAskOrder = createTakerOrder({ + isOrderAsk: true, + taker: takerAskUser.address, + tokenId: BigNumber.from(tokenId), + price: parseEther("3"), + minPercentageToAsk: constants.Zero, + params: defaultAbiCoder.encode(["bytes32[]"], [hexProof]), + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + await expect( + contracts.bendExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder) + ).to.be.revertedWith("Strategy: execution invalid"); + } + } + }); + + it("Cannot match if wrong side", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyAnyItemInASetForFixedPrice.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, // these parameters are used after it reverts + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = { + isOrderAsk: false, + isTakerFirst: false, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + taker: takerBidUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) + ).to.be.revertedWith("Strategy: execution invalid"); + }); + } +); diff --git a/test/strategies/strategyDutchAuction.test.ts b/test/strategies/strategyDutchAuction.test.ts new file mode 100644 index 0000000..f1f8f45 --- /dev/null +++ b/test/strategies/strategyDutchAuction.test.ts @@ -0,0 +1,299 @@ +import { assert, expect } from "chai"; +import { BigNumber, constants, utils } from "ethers"; +import { ethers } from "hardhat"; + +import { MakerOrderWithSignature, TakerOrder } from "../helpers/order-types"; +import { createMakerOrder, createTakerOrder } from "../helpers/order-helper"; +import { computeOrderHash } from "../helpers/signature-helper"; +import { Contracts, Env, makeSuite, Snapshots } from "../_setup"; +import { increaseTo } from "../helpers/block-traveller"; + +const { defaultAbiCoder, parseEther } = utils; + +makeSuite("Strategy - Dutch Auction", (contracts: Contracts, env: Env, snapshots: Snapshots) => { + let startTimeOrder: BigNumber; + let endTimeOrder: BigNumber; + const emptyEncodedBytes = defaultAbiCoder.encode([], []); + + beforeEach(async () => { + // Set up defaults startTime/endTime (for orders) + startTimeOrder = BigNumber.from((await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp); + endTimeOrder = startTimeOrder.add(BigNumber.from("1000")); + }); + + afterEach(async () => { + await snapshots.revert("setup"); + }); + + it("ERC721 - Buyer pays the exact auction price", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + endTimeOrder = startTimeOrder.add(BigNumber.from("1000")); + + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + price: parseEther("1"), + tokenId: constants.Zero, + amount: constants.One, + strategy: contracts.strategyDutchAuction.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: defaultAbiCoder.encode(["uint256"], [parseEther("5")]), + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: constants.Zero, + price: BigNumber.from(parseEther("3").toString()), + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + // User 2 cannot buy since the current auction price is not 3 + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Strategy: execution invalid"); + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) + ).to.be.revertedWith("Strategy: execution invalid"); + + // Advance time to half time of the auction (3 is between 5 and 1) + const midTimeOrder = startTimeOrder.add("500"); + await increaseTo(midTimeOrder); + + const tx = await contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyDutchAuction.address, + makerAskOrder.currency, + makerAskOrder.collection, + takerBidOrder.tokenId, + makerAskOrder.amount, + takerBidOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf("0"), takerBidUser.address); + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + }); + + it("ERC1155 - Buyer overpays", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + endTimeOrder = startTimeOrder.add("1000"); + + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC1155.address, + price: parseEther("1"), + tokenId: constants.One, + amount: constants.Two, + strategy: contracts.strategyDutchAuction.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: defaultAbiCoder.encode(["uint256"], [parseEther("5")]), + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: constants.One, + price: BigNumber.from(parseEther("4.5").toString()), + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + // Advance time to half time of the auction (3 is between 5 and 1) + const midTimeOrder = startTimeOrder.add("500"); + await increaseTo(midTimeOrder); + + // User 2 buys with 4.5 WETH (when auction price was at 3 WETH) + const tx = await contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyDutchAuction.address, + makerAskOrder.currency, + makerAskOrder.collection, + takerBidOrder.tokenId, + makerAskOrder.amount, + takerBidOrder.price + ); + + // Verify amount transfered to the protocol fee (user1) is (protocolFee) * 4.5 WETH + const protocolFee = await contracts.strategyDutchAuction.PROTOCOL_FEE(); + await expect(tx) + .to.emit(contracts.weth, "Transfer") + .withArgs(takerBidUser.address, env.feeRecipient.address, takerBidOrder.price.mul(protocolFee).div("10000")); + + // User 2 had minted 2 tokenId=1 so he has 4 + assert.deepEqual(await contracts.mockERC1155.balanceOf(takerBidUser.address, "1"), BigNumber.from("4")); + }); + + it("Revert if start price is lower than end price", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + let makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyDutchAuction.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: defaultAbiCoder.encode(["uint256", "uint256"], [parseEther("3"), parseEther("5")]), // startPrice/endPrice + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerBidOrder: TakerOrder = { + isOrderAsk: false, + taker: takerBidUser.address, + tokenId: makerAskOrder.tokenId, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) + ).to.be.revertedWith("Dutch Auction: start price must be greater than end price"); + + // EndTimeOrder is 50 seconds after startTimeOrder + endTimeOrder = startTimeOrder.add(BigNumber.from("50")); + + makerAskOrder = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyDutchAuction.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: defaultAbiCoder.encode(["uint256", "uint256"], [parseEther("5"), parseEther("3")]), // startPrice/endPrice + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + await expect( + contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) + ).to.be.revertedWith("Dutch Auction: length must be longer"); + }); + + it("Cannot match if wrong side", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const makerBidOrder = await createMakerOrder({ + isOrderAsk: false, + maker: takerBidUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("3"), + amount: constants.One, + strategy: contracts.strategyDutchAuction.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: defaultAbiCoder.encode(["uint256"], [parseEther("5")]), // startPrice + signerUser: takerBidUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerAskOrder: TakerOrder = { + isOrderAsk: true, + taker: makerAskUser.address, + tokenId: makerBidOrder.tokenId, + price: makerBidOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(makerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder) + ).to.be.revertedWith("Strategy: execution invalid"); + }); + + it("Min Auction length creates revertion as expected", async () => { + await expect( + contracts.strategyDutchAuction.connect(env.admin).updateMinimumAuctionLength("899") + ).to.be.revertedWith("Owner: auction length must be > 15 min"); + + const StrategyDutchAuction = await ethers.getContractFactory("StrategyDutchAuction"); + await expect(StrategyDutchAuction.connect(env.admin).deploy("900", "899")).to.be.revertedWith( + "Owner: auction length must be > 15 min" + ); + }); + + it("Owner functions work as expected", async () => { + const tx = await contracts.strategyDutchAuction.connect(env.admin).updateMinimumAuctionLength("1000"); + await expect(tx).to.emit(contracts.strategyDutchAuction, "NewMinimumAuctionLengthInSeconds").withArgs("1000"); + }); + + it("Owner functions are only callable by owner", async () => { + const notAdminUser = env.accounts[3]; + + await expect( + contracts.strategyDutchAuction.connect(notAdminUser).updateMinimumAuctionLength("500") + ).to.be.revertedWith("Ownable: caller is not the owner"); + }); +}); diff --git a/test/strategies/strategyPrivateSale.test.ts b/test/strategies/strategyPrivateSale.test.ts new file mode 100644 index 0000000..8d39241 --- /dev/null +++ b/test/strategies/strategyPrivateSale.test.ts @@ -0,0 +1,236 @@ +import { assert, expect } from "chai"; +import { BigNumber, constants, utils } from "ethers"; +import { ethers } from "hardhat"; + +import { MakerOrderWithSignature, TakerOrder } from "../helpers/order-types"; +import { createMakerOrder, createTakerOrder } from "../helpers/order-helper"; +import { computeOrderHash } from "../helpers/signature-helper"; +import { Contracts, Env, makeSuite, Snapshots } from "../_setup"; + +const { defaultAbiCoder, parseEther } = utils; + +makeSuite("Strategy - PrivateSale", (contracts: Contracts, env: Env, snapshots: Snapshots) => { + // Other global variables + let startTimeOrder: BigNumber; + let endTimeOrder: BigNumber; + + const emptyEncodedBytes = defaultAbiCoder.encode([], []); + + beforeEach(async () => { + // Set up defaults startTime/endTime (for orders) + startTimeOrder = BigNumber.from((await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp); + endTimeOrder = startTimeOrder.add(BigNumber.from("1000")); + }); + + afterEach(async () => { + await snapshots.revert("setup"); + }); + + it("ERC721 - No platform fee, only target can buy", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + const wrongUser = env.accounts[3]; + + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("5"), + amount: constants.One, + strategy: contracts.strategyPrivateSale.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: defaultAbiCoder.encode(["address"], [takerBidUser.address]), + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + let takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: wrongUser.address, + tokenId: constants.Zero, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + // User 3 cannot buy since the order target is only taker user + await expect( + contracts.bendExchange.connect(wrongUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Strategy: execution invalid"); + + await expect( + contracts.bendExchange.connect(wrongUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) + ).to.be.revertedWith("Strategy: execution invalid"); + + takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + price: makerAskOrder.price, + tokenId: constants.Zero, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + assert.deepEqual(await contracts.weth.balanceOf(env.feeRecipient.address), constants.Zero); + + const tx = await contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyPrivateSale.address, + makerAskOrder.currency, + makerAskOrder.collection, + takerBidOrder.tokenId, + makerAskOrder.amount, + makerAskOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf(constants.Zero), takerBidUser.address); + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + // Verify balance of treasury (aka env.feeRecipient) is 0 + assert.deepEqual(await contracts.weth.balanceOf(env.feeRecipient.address), constants.Zero); + }); + + it("ERC721 - No platform fee, only target can buy", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + const wrongUser = env.accounts[3]; + + const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: true, + maker: makerAskUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + price: parseEther("5"), + amount: constants.One, + strategy: contracts.strategyPrivateSale.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: defaultAbiCoder.encode(["address"], [takerBidUser.address]), + signerUser: makerAskUser, + verifyingContract: contracts.bendExchange.address, + }); + + let takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: wrongUser.address, + tokenId: constants.Zero, + price: makerAskOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + // User 3 cannot buy since the order target is only taker user + await expect( + contracts.bendExchange.connect(wrongUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { + value: takerBidOrder.price, + }) + ).to.be.revertedWith("Strategy: execution invalid"); + + await expect( + contracts.bendExchange.connect(wrongUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) + ).to.be.revertedWith("Strategy: execution invalid"); + + takerBidOrder = createTakerOrder({ + isOrderAsk: false, + taker: takerBidUser.address, + price: makerAskOrder.price, + tokenId: constants.Zero, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }); + + assert.deepEqual(await contracts.weth.balanceOf(env.feeRecipient.address), constants.Zero); + + const tx = await contracts.bendExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); + await expect(tx) + .to.emit(contracts.bendExchange, "TakerBid") + .withArgs( + computeOrderHash(makerAskOrder), + makerAskOrder.nonce, + takerBidUser.address, + makerAskUser.address, + contracts.strategyPrivateSale.address, + makerAskOrder.currency, + makerAskOrder.collection, + takerBidOrder.tokenId, + makerAskOrder.amount, + makerAskOrder.price + ); + + assert.equal(await contracts.mockERC721.ownerOf(constants.Zero), takerBidUser.address); + assert.isTrue( + await contracts.bendExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) + ); + // Verify balance of treasury (aka env.feeRecipient) is 0 + assert.deepEqual(await contracts.weth.balanceOf(env.feeRecipient.address), constants.Zero); + }); + + it("Cannot match if wrong side", async () => { + const makerAskUser = env.accounts[1]; + const takerBidUser = env.accounts[2]; + + const makerBidOrder: MakerOrderWithSignature = await createMakerOrder({ + isOrderAsk: false, + maker: takerBidUser.address, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + collection: contracts.mockERC721.address, + tokenId: constants.Zero, + amount: constants.One, + price: parseEther("3"), + strategy: contracts.strategyPrivateSale.address, + currency: contracts.weth.address, + nonce: constants.Zero, + startTime: startTimeOrder, + endTime: endTimeOrder, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + signerUser: takerBidUser, + verifyingContract: contracts.bendExchange.address, + }); + + const takerAskOrder: TakerOrder = { + isOrderAsk: true, + taker: makerAskUser.address, + tokenId: makerBidOrder.tokenId, + price: makerBidOrder.price, + minPercentageToAsk: constants.Zero, + params: emptyEncodedBytes, + interceptor: constants.AddressZero, + interceptorExtra: emptyEncodedBytes, + }; + + await expect( + contracts.bendExchange.connect(makerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder) + ).to.be.revertedWith("Strategy: execution invalid"); + }); +}); diff --git a/test/strategyAnyItemFromCollectionForFixedPrice.test.ts b/test/strategyAnyItemFromCollectionForFixedPrice.test.ts deleted file mode 100644 index 32277d8..0000000 --- a/test/strategyAnyItemFromCollectionForFixedPrice.test.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { assert, expect } from "chai"; -import { BigNumber, constants, Contract, utils } from "ethers"; -import { ethers } from "hardhat"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; - -import { MakerOrderWithSignature, TakerOrder } from "./helpers/order-types"; -import { createMakerOrder, createTakerOrder } from "./helpers/order-helper"; -import { computeDomainSeparator, computeOrderHash } from "./helpers/signature-helper"; -import { setUp } from "./test-setup"; -import { tokenSetUp } from "./token-set-up"; - -const { defaultAbiCoder, parseEther } = utils; - -describe("Strategy - AnyItemFromCollectionForFixedPrice ('Collection orders')", () => { - // Mock contracts - let mockERC721: Contract; - let mockERC721WithRoyalty: Contract; - let mockERC1155: Contract; - let weth: Contract; - - // Exchange contracts - let transferManagerERC721: Contract; - let transferManagerERC1155: Contract; - let looksRareExchange: Contract; - - // Strategy contract - let strategyAnyItemFromCollectionForFixedPrice: Contract; - - // Other global variables - let standardProtocolFee: BigNumber; - let royaltyFeeLimit: BigNumber; - let accounts: SignerWithAddress[]; - let admin: SignerWithAddress; - let feeRecipient: SignerWithAddress; - let royaltyCollector: SignerWithAddress; - let startTimeOrder: BigNumber; - let endTimeOrder: BigNumber; - - beforeEach(async () => { - accounts = await ethers.getSigners(); - admin = accounts[0]; - feeRecipient = accounts[19]; - royaltyCollector = accounts[15]; - standardProtocolFee = BigNumber.from("200"); - royaltyFeeLimit = BigNumber.from("9500"); // 95% - [ - weth, - mockERC721, - mockERC1155, - , - mockERC721WithRoyalty, - , - , - , - transferManagerERC721, - transferManagerERC1155, - , - looksRareExchange, - , - strategyAnyItemFromCollectionForFixedPrice, - , - , - , - , - , - , - ] = await setUp(admin, feeRecipient, royaltyCollector, standardProtocolFee, royaltyFeeLimit); - - await tokenSetUp( - accounts.slice(1, 10), - weth, - mockERC721, - mockERC721WithRoyalty, - mockERC1155, - looksRareExchange, - transferManagerERC721, - transferManagerERC1155 - ); - - // Verify the domain separator is properly computed - assert.equal(await looksRareExchange.DOMAIN_SEPARATOR(), computeDomainSeparator(looksRareExchange.address)); - - // Set up defaults startTime/endTime (for orders) - startTimeOrder = BigNumber.from((await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp); - endTimeOrder = startTimeOrder.add(BigNumber.from("1000")); - }); - - it("ERC721 - MakerBid order is matched by TakerAsk order", async () => { - const makerBidUser = accounts[1]; - const takerAskUser = accounts[5]; - - const makerBidOrder: MakerOrderWithSignature = await createMakerOrder({ - isOrderAsk: false, - signer: makerBidUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, // Not used - price: parseEther("3"), - amount: constants.One, - strategy: strategyAnyItemFromCollectionForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerBidUser, - verifyingContract: looksRareExchange.address, - }); - - const takerAskOrder = createTakerOrder({ - isOrderAsk: true, - taker: takerAskUser.address, - tokenId: BigNumber.from("4"), - price: makerBidOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - const tx = await looksRareExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); - await expect(tx) - .to.emit(looksRareExchange, "TakerAsk") - .withArgs( - computeOrderHash(makerBidOrder), - makerBidOrder.nonce, - takerAskUser.address, - makerBidUser.address, - strategyAnyItemFromCollectionForFixedPrice.address, - makerBidOrder.currency, - makerBidOrder.collection, - takerAskOrder.tokenId, - makerBidOrder.amount, - makerBidOrder.price - ); - - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(makerBidUser.address, makerBidOrder.nonce) - ); - }); - - it("ERC1155 - MakerBid order is matched by TakerAsk order", async () => { - const makerBidUser = accounts[1]; - const takerAskUser = accounts[2]; - - const makerBidOrder: MakerOrderWithSignature = await createMakerOrder({ - isOrderAsk: false, - signer: makerBidUser.address, - collection: mockERC1155.address, - tokenId: constants.Zero, // not used - price: parseEther("3"), - amount: constants.Two, - strategy: strategyAnyItemFromCollectionForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerBidUser, - verifyingContract: looksRareExchange.address, - }); - - const takerAskOrder = createTakerOrder({ - isOrderAsk: true, - taker: takerAskUser.address, - price: makerBidOrder.price, - tokenId: BigNumber.from("2"), - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - const tx = await looksRareExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); - await expect(tx) - .to.emit(looksRareExchange, "TakerAsk") - .withArgs( - computeOrderHash(makerBidOrder), - makerBidOrder.nonce, - takerAskUser.address, - makerBidUser.address, - strategyAnyItemFromCollectionForFixedPrice.address, - makerBidOrder.currency, - makerBidOrder.collection, - takerAskOrder.tokenId, - makerBidOrder.amount, - makerBidOrder.price - ); - - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(makerBidUser.address, makerBidOrder.nonce) - ); - }); - - it("Cannot match if wrong side", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyAnyItemFromCollectionForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: makerAskOrder.tokenId, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) - ).to.be.revertedWith("Strategy: Execution invalid"); - }); -}); diff --git a/test/strategyAnyItemInASetForAFixedPrice.test.ts b/test/strategyAnyItemInASetForAFixedPrice.test.ts deleted file mode 100644 index 379630a..0000000 --- a/test/strategyAnyItemInASetForAFixedPrice.test.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { assert, expect } from "chai"; -import { BigNumber, constants, Contract, utils } from "ethers"; -import { ethers } from "hardhat"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { MerkleTree } from "merkletreejs"; -/* eslint-disable node/no-extraneous-import */ -import { keccak256 } from "js-sha3"; - -import { MakerOrderWithSignature } from "./helpers/order-types"; -import { createMakerOrder, createTakerOrder } from "./helpers/order-helper"; -import { computeDomainSeparator, computeOrderHash } from "./helpers/signature-helper"; -import { setUp } from "./test-setup"; -import { tokenSetUp } from "./token-set-up"; - -const { defaultAbiCoder, parseEther } = utils; - -describe("Strategy - AnyItemInASetForFixedPrice ('Trait orders')", () => { - // Mock contracts - let mockERC721: Contract; - let mockERC721WithRoyalty: Contract; - let mockERC1155: Contract; - let weth: Contract; - - // Exchange contracts - let transferManagerERC721: Contract; - let transferManagerERC1155: Contract; - let looksRareExchange: Contract; - - // Strategy contract - let strategyAnyItemInASetForFixedPrice: Contract; - - // Other global variables - let standardProtocolFee: BigNumber; - let royaltyFeeLimit: BigNumber; - let accounts: SignerWithAddress[]; - let admin: SignerWithAddress; - let feeRecipient: SignerWithAddress; - let royaltyCollector: SignerWithAddress; - let startTimeOrder: BigNumber; - let endTimeOrder: BigNumber; - - beforeEach(async () => { - accounts = await ethers.getSigners(); - admin = accounts[0]; - feeRecipient = accounts[19]; - royaltyCollector = accounts[15]; - standardProtocolFee = BigNumber.from("200"); - royaltyFeeLimit = BigNumber.from("9500"); // 95% - [ - weth, - mockERC721, - mockERC1155, - , - mockERC721WithRoyalty, - , - , - , - transferManagerERC721, - transferManagerERC1155, - , - looksRareExchange, - , - , - , - , - strategyAnyItemInASetForFixedPrice, - , - , - , - ] = await setUp(admin, feeRecipient, royaltyCollector, standardProtocolFee, royaltyFeeLimit); - - await tokenSetUp( - accounts.slice(1, 10), - weth, - mockERC721, - mockERC721WithRoyalty, - mockERC1155, - looksRareExchange, - transferManagerERC721, - transferManagerERC1155 - ); - - // Verify the domain separator is properly computed - assert.equal(await looksRareExchange.DOMAIN_SEPARATOR(), computeDomainSeparator(looksRareExchange.address)); - - // Set up defaults startTime/endTime (for orders) - startTimeOrder = BigNumber.from((await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp); - endTimeOrder = startTimeOrder.add(BigNumber.from("1000")); - }); - - it("ERC721 - MakerAsk order is matched by TakerBid order", async () => { - const takerAskUser = accounts[3]; // has tokenId=2 - const makerBidUser = accounts[1]; - - // User wishes to buy either tokenId = 0, 2, 3, or 12 - const eligibleTokenIds = ["0", "2", "3", "12"]; - - // Compute the leaves using Solidity keccak256 (Equivalent of keccak256 with abi.encodePacked) and converts to hex - const leaves = eligibleTokenIds.map((x) => "0x" + utils.solidityKeccak256(["uint256"], [x]).substr(2)); - - // Compute MerkleTree based on the computed leaves - const tree = new MerkleTree(leaves, keccak256, { sortPairs: true }); - - // Compute the proof for index=1 (aka tokenId=2) - const hexProof = tree.getHexProof(leaves[1], 1); - - // Compute the root of the tree - const hexRoot = tree.getHexRoot(); - - // Verify leaf is matched in the tree with the computed root - assert.isTrue(tree.verify(hexProof, leaves[1], hexRoot)); - - const makerBidOrder: MakerOrderWithSignature = await createMakerOrder({ - isOrderAsk: false, - signer: makerBidUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyAnyItemInASetForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode(["bytes32"], [hexRoot]), - signerUser: makerBidUser, - verifyingContract: looksRareExchange.address, - }); - - const takerAskOrder = createTakerOrder({ - isOrderAsk: true, - taker: takerAskUser.address, - tokenId: BigNumber.from("2"), - price: makerBidOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode(["bytes32[]"], [hexProof]), - }); - - const tx = await looksRareExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder); - await expect(tx) - .to.emit(looksRareExchange, "TakerAsk") - .withArgs( - computeOrderHash(makerBidOrder), - makerBidOrder.nonce, - takerAskUser.address, - makerBidUser.address, - strategyAnyItemInASetForFixedPrice.address, - makerBidOrder.currency, - makerBidOrder.collection, - takerAskOrder.tokenId, - makerBidOrder.amount, - makerBidOrder.price - ); - - assert.equal(await mockERC721.ownerOf("2"), makerBidUser.address); - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(makerBidUser.address, makerBidOrder.nonce) - ); - }); - - it("ERC721 - TokenIds not in the set cannot be sold", async () => { - const takerAskUser = accounts[3]; // has tokenId=2 - const makerBidUser = accounts[1]; - - // User wishes to buy either tokenId = 1, 2, 3, 4, or 12 - const eligibleTokenIds = ["1", "2", "3", "4", "12"]; - - // Compute the leaves using Solidity keccak256 (Equivalent of keccak256 with abi.encodePacked) and converts to hex - const leaves = eligibleTokenIds.map((x) => "0x" + utils.solidityKeccak256(["uint256"], [x]).substr(2)); - - // Compute MerkleTree based on the computed leaves - const tree = new MerkleTree(leaves, keccak256, { sortPairs: true }); - - // Compute the proof for index=1 (aka tokenId=2) - const hexProof = tree.getHexProof(leaves[1], 1); - - // Compute the root of the tree - const hexRoot = tree.getHexRoot(); - - // Verify leaf is matched in the tree with the computed root - assert.isTrue(tree.verify(hexProof, leaves[1], hexRoot)); - - const makerBidOrder: MakerOrderWithSignature = await createMakerOrder({ - isOrderAsk: false, - signer: makerBidUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, // not used - price: parseEther("3"), - amount: constants.One, - strategy: strategyAnyItemInASetForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode(["bytes32"], [hexRoot]), - signerUser: makerBidUser, - verifyingContract: looksRareExchange.address, - }); - - for (const tokenId of Array.from(Array(9).keys())) { - // If the tokenId is not included, it skips - if (!eligibleTokenIds.includes(tokenId.toString())) { - const takerAskOrder = createTakerOrder({ - isOrderAsk: true, - taker: takerAskUser.address, - tokenId: BigNumber.from(tokenId), - price: parseEther("3"), - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode(["bytes32[]"], [hexProof]), - }); - - await expect( - looksRareExchange.connect(takerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder) - ).to.be.revertedWith("Strategy: Execution invalid"); - } - } - }); - - it("Cannot match if wrong side", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - const makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyAnyItemInASetForFixedPrice.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), // these parameters are used after it reverts - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder = { - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: makerAskOrder.tokenId, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) - ).to.be.revertedWith("Strategy: Execution invalid"); - }); -}); diff --git a/test/strategyDutchAuction.test.ts b/test/strategyDutchAuction.test.ts deleted file mode 100644 index d837bbf..0000000 --- a/test/strategyDutchAuction.test.ts +++ /dev/null @@ -1,342 +0,0 @@ -import { assert, expect } from "chai"; -import { BigNumber, constants, Contract, utils } from "ethers"; -import { ethers } from "hardhat"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; - -import { MakerOrderWithSignature, TakerOrder } from "./helpers/order-types"; -import { createMakerOrder, createTakerOrder } from "./helpers/order-helper"; -import { computeDomainSeparator, computeOrderHash } from "./helpers/signature-helper"; -import { setUp } from "./test-setup"; -import { tokenSetUp } from "./token-set-up"; -import { increaseTo } from "./helpers/block-traveller"; - -const { defaultAbiCoder, parseEther } = utils; - -describe("Strategy - Dutch Auction", () => { - // Mock contracts - let mockERC721: Contract; - let mockERC721WithRoyalty: Contract; - let mockERC1155: Contract; - let weth: Contract; - - // Exchange contracts - let transferManagerERC721: Contract; - let transferManagerERC1155: Contract; - let looksRareExchange: Contract; - - // Strategy contract - let strategyDutchAuction: Contract; - - // Other global variables - let standardProtocolFee: BigNumber; - let royaltyFeeLimit: BigNumber; - let accounts: SignerWithAddress[]; - let admin: SignerWithAddress; - let feeRecipient: SignerWithAddress; - let royaltyCollector: SignerWithAddress; - let startTimeOrder: BigNumber; - let endTimeOrder: BigNumber; - - beforeEach(async () => { - accounts = await ethers.getSigners(); - admin = accounts[0]; - feeRecipient = accounts[19]; - royaltyCollector = accounts[15]; - standardProtocolFee = BigNumber.from("200"); - royaltyFeeLimit = BigNumber.from("9500"); // 95% - [ - weth, - mockERC721, - mockERC1155, - , - mockERC721WithRoyalty, - , - , - , - transferManagerERC721, - transferManagerERC1155, - , - looksRareExchange, - , - , - strategyDutchAuction, - , - , - , - , - , - ] = await setUp(admin, feeRecipient, royaltyCollector, standardProtocolFee, royaltyFeeLimit); - - await tokenSetUp( - accounts.slice(1, 10), - weth, - mockERC721, - mockERC721WithRoyalty, - mockERC1155, - looksRareExchange, - transferManagerERC721, - transferManagerERC1155 - ); - - // Verify the domain separator is properly computed - assert.equal(await looksRareExchange.DOMAIN_SEPARATOR(), computeDomainSeparator(looksRareExchange.address)); - - // Set up defaults startTime/endTime (for orders) - startTimeOrder = BigNumber.from((await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp); - endTimeOrder = startTimeOrder.add(BigNumber.from("1000")); - }); - - it("ERC721 - Buyer pays the exact auction price", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - endTimeOrder = startTimeOrder.add(BigNumber.from("1000")); - - const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - price: parseEther("1"), - tokenId: constants.Zero, - amount: constants.One, - strategy: strategyDutchAuction.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode(["uint256"], [parseEther("5")]), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder = createTakerOrder({ - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: constants.Zero, - price: BigNumber.from(parseEther("3").toString()), - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - // User 2 cannot buy since the current auction price is not 3 - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { - value: takerBidOrder.price, - }) - ).to.be.revertedWith("Strategy: Execution invalid"); - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) - ).to.be.revertedWith("Strategy: Execution invalid"); - - // Advance time to half time of the auction (3 is between 5 and 1) - const midTimeOrder = startTimeOrder.add("500"); - await increaseTo(midTimeOrder); - - const tx = await looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); - await expect(tx) - .to.emit(looksRareExchange, "TakerBid") - .withArgs( - computeOrderHash(makerAskOrder), - makerAskOrder.nonce, - takerBidUser.address, - makerAskUser.address, - strategyDutchAuction.address, - makerAskOrder.currency, - makerAskOrder.collection, - takerBidOrder.tokenId, - makerAskOrder.amount, - takerBidOrder.price - ); - - assert.equal(await mockERC721.ownerOf("0"), takerBidUser.address); - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) - ); - }); - - it("ERC1155 - Buyer overpays", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - endTimeOrder = startTimeOrder.add("1000"); - - const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC1155.address, - price: parseEther("1"), - tokenId: constants.One, - amount: constants.Two, - strategy: strategyDutchAuction.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode(["uint256"], [parseEther("5")]), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder = createTakerOrder({ - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: constants.One, - price: BigNumber.from(parseEther("4.5").toString()), - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - // Advance time to half time of the auction (3 is between 5 and 1) - const midTimeOrder = startTimeOrder.add("500"); - await increaseTo(midTimeOrder); - - // User 2 buys with 4.5 WETH (when auction price was at 3 WETH) - const tx = await looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); - await expect(tx) - .to.emit(looksRareExchange, "TakerBid") - .withArgs( - computeOrderHash(makerAskOrder), - makerAskOrder.nonce, - takerBidUser.address, - makerAskUser.address, - strategyDutchAuction.address, - makerAskOrder.currency, - makerAskOrder.collection, - takerBidOrder.tokenId, - makerAskOrder.amount, - takerBidOrder.price - ); - - // Verify amount transfered to the protocol fee (user1) is (protocolFee) * 4.5 WETH - const protocolFee = await strategyDutchAuction.PROTOCOL_FEE(); - await expect(tx) - .to.emit(weth, "Transfer") - .withArgs(takerBidUser.address, feeRecipient.address, takerBidOrder.price.mul(protocolFee).div("10000")); - - // User 2 had minted 2 tokenId=1 so he has 4 - assert.deepEqual(await mockERC1155.balanceOf(takerBidUser.address, "1"), BigNumber.from("4")); - }); - - it("Revert if start price is lower than end price", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - let makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyDutchAuction.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode(["uint256", "uint256"], [parseEther("3"), parseEther("5")]), // startPrice/endPrice - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - const takerBidOrder: TakerOrder = { - isOrderAsk: false, - taker: takerBidUser.address, - tokenId: makerAskOrder.tokenId, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) - ).to.be.revertedWith("Dutch Auction: Start price must be greater than end price"); - - // EndTimeOrder is 50 seconds after startTimeOrder - endTimeOrder = startTimeOrder.add(BigNumber.from("50")); - - makerAskOrder = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyDutchAuction.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode(["uint256", "uint256"], [parseEther("5"), parseEther("3")]), // startPrice/endPrice - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - await expect( - looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) - ).to.be.revertedWith("Dutch Auction: Length must be longer"); - }); - - it("Cannot match if wrong side", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - const makerBidOrder = await createMakerOrder({ - isOrderAsk: false, - signer: takerBidUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("3"), - amount: constants.One, - strategy: strategyDutchAuction.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode(["uint256"], [parseEther("5")]), // startPrice - signerUser: takerBidUser, - verifyingContract: looksRareExchange.address, - }); - - const takerAskOrder: TakerOrder = { - isOrderAsk: true, - taker: makerAskUser.address, - tokenId: makerBidOrder.tokenId, - price: makerBidOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - await expect( - looksRareExchange.connect(makerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder) - ).to.be.revertedWith("Strategy: Execution invalid"); - }); - - it("Min Auction length creates revertion as expected", async () => { - await expect(strategyDutchAuction.connect(admin).updateMinimumAuctionLength("899")).to.be.revertedWith( - "Owner: Auction length must be > 15 min" - ); - - const StrategyDutchAuction = await ethers.getContractFactory("StrategyDutchAuction"); - await expect(StrategyDutchAuction.connect(admin).deploy("900", "899")).to.be.revertedWith( - "Owner: Auction length must be > 15 min" - ); - }); - - it("Owner functions work as expected", async () => { - const tx = await strategyDutchAuction.connect(admin).updateMinimumAuctionLength("1000"); - await expect(tx).to.emit(strategyDutchAuction, "NewMinimumAuctionLengthInSeconds").withArgs("1000"); - }); - - it("Owner functions are only callable by owner", async () => { - const notAdminUser = accounts[3]; - - await expect(strategyDutchAuction.connect(notAdminUser).updateMinimumAuctionLength("500")).to.be.revertedWith( - "Ownable: caller is not the owner" - ); - }); -}); diff --git a/test/strategyPrivateSale.test.ts b/test/strategyPrivateSale.test.ts deleted file mode 100644 index d0186c0..0000000 --- a/test/strategyPrivateSale.test.ts +++ /dev/null @@ -1,279 +0,0 @@ -import { assert, expect } from "chai"; -import { BigNumber, constants, Contract, utils } from "ethers"; -import { ethers } from "hardhat"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; - -import { MakerOrderWithSignature, TakerOrder } from "./helpers/order-types"; -import { createMakerOrder, createTakerOrder } from "./helpers/order-helper"; -import { computeDomainSeparator, computeOrderHash } from "./helpers/signature-helper"; -import { setUp } from "./test-setup"; -import { tokenSetUp } from "./token-set-up"; - -const { defaultAbiCoder, parseEther } = utils; - -describe("Strategy - PrivateSale", () => { - // Mock contracts - let mockERC721: Contract; - let mockERC721WithRoyalty: Contract; - let mockERC1155: Contract; - let weth: Contract; - - // Exchange contracts - let transferManagerERC721: Contract; - let transferManagerERC1155: Contract; - let looksRareExchange: Contract; - - // Strategy contract - let strategyPrivateSale: Contract; - - // Other global variables - let standardProtocolFee: BigNumber; - let royaltyFeeLimit: BigNumber; - let accounts: SignerWithAddress[]; - let admin: SignerWithAddress; - let feeRecipient: SignerWithAddress; - let royaltyCollector: SignerWithAddress; - let startTimeOrder: BigNumber; - let endTimeOrder: BigNumber; - - beforeEach(async () => { - accounts = await ethers.getSigners(); - admin = accounts[0]; - feeRecipient = accounts[19]; - royaltyCollector = accounts[15]; - standardProtocolFee = BigNumber.from("200"); - royaltyFeeLimit = BigNumber.from("9500"); // 95% - [ - weth, - mockERC721, - mockERC1155, - , - mockERC721WithRoyalty, - , - , - , - transferManagerERC721, - transferManagerERC1155, - , - looksRareExchange, - , - , - , - strategyPrivateSale, - , - , - , - , - ] = await setUp(admin, feeRecipient, royaltyCollector, standardProtocolFee, royaltyFeeLimit); - - await tokenSetUp( - accounts.slice(1, 10), - weth, - mockERC721, - mockERC721WithRoyalty, - mockERC1155, - looksRareExchange, - transferManagerERC721, - transferManagerERC1155 - ); - - // Verify the domain separator is properly computed - assert.equal(await looksRareExchange.DOMAIN_SEPARATOR(), computeDomainSeparator(looksRareExchange.address)); - - // Set up defaults startTime/endTime (for orders) - startTimeOrder = BigNumber.from((await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp); - endTimeOrder = startTimeOrder.add(BigNumber.from("1000")); - }); - - it("ERC721 - No platform fee, only target can buy", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - const wrongUser = accounts[3]; - - const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("5"), - amount: constants.One, - strategy: strategyPrivateSale.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode(["address"], [takerBidUser.address]), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - let takerBidOrder = createTakerOrder({ - isOrderAsk: false, - taker: wrongUser.address, - tokenId: constants.Zero, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - // User 3 cannot buy since the order target is only taker user - await expect( - looksRareExchange.connect(wrongUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { - value: takerBidOrder.price, - }) - ).to.be.revertedWith("Strategy: Execution invalid"); - - await expect( - looksRareExchange.connect(wrongUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) - ).to.be.revertedWith("Strategy: Execution invalid"); - - takerBidOrder = createTakerOrder({ - isOrderAsk: false, - taker: takerBidUser.address, - price: makerAskOrder.price, - tokenId: constants.Zero, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - assert.deepEqual(await weth.balanceOf(feeRecipient.address), constants.Zero); - - const tx = await looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); - await expect(tx) - .to.emit(looksRareExchange, "TakerBid") - .withArgs( - computeOrderHash(makerAskOrder), - makerAskOrder.nonce, - takerBidUser.address, - makerAskUser.address, - strategyPrivateSale.address, - makerAskOrder.currency, - makerAskOrder.collection, - takerBidOrder.tokenId, - makerAskOrder.amount, - makerAskOrder.price - ); - - assert.equal(await mockERC721.ownerOf(constants.Zero), takerBidUser.address); - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) - ); - // Verify balance of treasury (aka feeRecipient) is 0 - assert.deepEqual(await weth.balanceOf(feeRecipient.address), constants.Zero); - }); - - it("ERC721 - No platform fee, only target can buy", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - const wrongUser = accounts[3]; - - const makerAskOrder: MakerOrderWithSignature = await createMakerOrder({ - isOrderAsk: true, - signer: makerAskUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - price: parseEther("5"), - amount: constants.One, - strategy: strategyPrivateSale.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode(["address"], [takerBidUser.address]), - signerUser: makerAskUser, - verifyingContract: looksRareExchange.address, - }); - - let takerBidOrder = createTakerOrder({ - isOrderAsk: false, - taker: wrongUser.address, - tokenId: constants.Zero, - price: makerAskOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - // User 3 cannot buy since the order target is only taker user - await expect( - looksRareExchange.connect(wrongUser).matchAskWithTakerBidUsingETHAndWETH(takerBidOrder, makerAskOrder, { - value: takerBidOrder.price, - }) - ).to.be.revertedWith("Strategy: Execution invalid"); - - await expect( - looksRareExchange.connect(wrongUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder) - ).to.be.revertedWith("Strategy: Execution invalid"); - - takerBidOrder = createTakerOrder({ - isOrderAsk: false, - taker: takerBidUser.address, - price: makerAskOrder.price, - tokenId: constants.Zero, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }); - - assert.deepEqual(await weth.balanceOf(feeRecipient.address), constants.Zero); - - const tx = await looksRareExchange.connect(takerBidUser).matchAskWithTakerBid(takerBidOrder, makerAskOrder); - await expect(tx) - .to.emit(looksRareExchange, "TakerBid") - .withArgs( - computeOrderHash(makerAskOrder), - makerAskOrder.nonce, - takerBidUser.address, - makerAskUser.address, - strategyPrivateSale.address, - makerAskOrder.currency, - makerAskOrder.collection, - takerBidOrder.tokenId, - makerAskOrder.amount, - makerAskOrder.price - ); - - assert.equal(await mockERC721.ownerOf(constants.Zero), takerBidUser.address); - assert.isTrue( - await looksRareExchange.isUserOrderNonceExecutedOrCancelled(makerAskUser.address, makerAskOrder.nonce) - ); - // Verify balance of treasury (aka feeRecipient) is 0 - assert.deepEqual(await weth.balanceOf(feeRecipient.address), constants.Zero); - }); - - it("Cannot match if wrong side", async () => { - const makerAskUser = accounts[1]; - const takerBidUser = accounts[2]; - - const makerBidOrder: MakerOrderWithSignature = await createMakerOrder({ - isOrderAsk: false, - signer: takerBidUser.address, - collection: mockERC721.address, - tokenId: constants.Zero, - amount: constants.One, - price: parseEther("3"), - strategy: strategyPrivateSale.address, - currency: weth.address, - nonce: constants.Zero, - startTime: startTimeOrder, - endTime: endTimeOrder, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - signerUser: takerBidUser, - verifyingContract: looksRareExchange.address, - }); - - const takerAskOrder: TakerOrder = { - isOrderAsk: true, - taker: makerAskUser.address, - tokenId: makerBidOrder.tokenId, - price: makerBidOrder.price, - minPercentageToAsk: constants.Zero, - params: defaultAbiCoder.encode([], []), - }; - - await expect( - looksRareExchange.connect(makerAskUser).matchBidWithTakerAsk(takerAskOrder, makerBidOrder) - ).to.be.revertedWith("Strategy: Execution invalid"); - }); -}); diff --git a/test/test-setup.ts b/test/test-setup.ts deleted file mode 100644 index 0b002a9..0000000 --- a/test/test-setup.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { BigNumber, constants, Contract } from "ethers"; -import { ethers } from "hardhat"; - -export async function setUp( - admin: SignerWithAddress, - feeRecipient: SignerWithAddress, - royaltyCollector: SignerWithAddress, - standardProtocolFee: BigNumber, - royaltyFeeLimit: BigNumber -): Promise { - /** 1. Deploy WETH, Mock ERC721, Mock ERC1155, Mock USDT, MockERC721WithRoyalty - */ - const WETH = await ethers.getContractFactory("WETH"); - const weth = await WETH.deploy(); - await weth.deployed(); - const MockERC721 = await ethers.getContractFactory("MockERC721"); - const mockERC721 = await MockERC721.deploy("Mock ERC721", "MERC721"); - await mockERC721.deployed(); - const MockERC1155 = await ethers.getContractFactory("MockERC1155"); - const mockERC1155 = await MockERC1155.deploy("uri/"); - await mockERC1155.deployed(); - const MockERC20 = await ethers.getContractFactory("MockERC20"); - const mockUSDT = await MockERC20.deploy("USD Tether", "USDT"); - await mockUSDT.deployed(); - const MockERC721WithRoyalty = await ethers.getContractFactory("MockERC721WithRoyalty"); - const mockERC721WithRoyalty = await MockERC721WithRoyalty.connect(royaltyCollector).deploy( - "Mock Royalty ERC721", - "MRC721", - "200" // 2% royalty fee - ); - await mockERC721WithRoyalty.deployed(); - - /** 2. Deploy ExecutionManager contract and add WETH to whitelisted currencies - */ - const CurrencyManager = await ethers.getContractFactory("CurrencyManager"); - const currencyManager = await CurrencyManager.deploy(); - await currencyManager.deployed(); - await currencyManager.connect(admin).addCurrency(weth.address); - - /** 3. Deploy ExecutionManager contract - */ - const ExecutionManager = await ethers.getContractFactory("ExecutionManager"); - const executionManager = await ExecutionManager.deploy(); - await executionManager.deployed(); - - /** 4. Deploy execution strategy contracts for trade execution - */ - const StrategyAnyItemFromCollectionForFixedPrice = await ethers.getContractFactory( - "StrategyAnyItemFromCollectionForFixedPrice" - ); - const strategyAnyItemFromCollectionForFixedPrice = await StrategyAnyItemFromCollectionForFixedPrice.deploy(200); - await strategyAnyItemFromCollectionForFixedPrice.deployed(); - const StrategyAnyItemInASetForFixedPrice = await ethers.getContractFactory("StrategyAnyItemInASetForFixedPrice"); - const strategyAnyItemInASetForFixedPrice = await StrategyAnyItemInASetForFixedPrice.deploy(standardProtocolFee); - await strategyAnyItemInASetForFixedPrice.deployed(); - const StrategyDutchAuction = await ethers.getContractFactory("StrategyDutchAuction"); - const strategyDutchAuction = await StrategyDutchAuction.deploy( - standardProtocolFee, - BigNumber.from("900") // 15 minutes - ); - await strategyDutchAuction.deployed(); - const StrategyPrivateSale = await ethers.getContractFactory("StrategyPrivateSale"); - const strategyPrivateSale = await StrategyPrivateSale.deploy(constants.Zero); - await strategyPrivateSale.deployed(); - const StrategyStandardSaleForFixedPrice = await ethers.getContractFactory("StrategyStandardSaleForFixedPrice"); - const strategyStandardSaleForFixedPrice = await StrategyStandardSaleForFixedPrice.deploy(standardProtocolFee); - await strategyStandardSaleForFixedPrice.deployed(); - - // Whitelist these five strategies - await executionManager.connect(admin).addStrategy(strategyStandardSaleForFixedPrice.address); - await executionManager.connect(admin).addStrategy(strategyAnyItemFromCollectionForFixedPrice.address); - await executionManager.connect(admin).addStrategy(strategyAnyItemInASetForFixedPrice.address); - await executionManager.connect(admin).addStrategy(strategyDutchAuction.address); - await executionManager.connect(admin).addStrategy(strategyPrivateSale.address); - - /** 5. Deploy RoyaltyFee Registry/Setter/Manager - */ - const RoyaltyFeeRegistry = await ethers.getContractFactory("RoyaltyFeeRegistry"); - const royaltyFeeRegistry = await RoyaltyFeeRegistry.deploy(royaltyFeeLimit); - await royaltyFeeRegistry.deployed(); - const RoyaltyFeeSetter = await ethers.getContractFactory("RoyaltyFeeSetter"); - const royaltyFeeSetter = await RoyaltyFeeSetter.deploy(royaltyFeeRegistry.address); - await royaltyFeeSetter.deployed(); - const RoyaltyFeeManager = await ethers.getContractFactory("RoyaltyFeeManager"); - const royaltyFeeManager = await RoyaltyFeeManager.deploy(royaltyFeeRegistry.address); - await royaltyFeeSetter.deployed(); - // Transfer ownership of RoyaltyFeeRegistry to RoyaltyFeeSetter - await royaltyFeeRegistry.connect(admin).transferOwnership(royaltyFeeSetter.address); - - /** 6. Deploy LooksRareExchange contract - */ - const LooksRareExchange = await ethers.getContractFactory("LooksRareExchange"); - const looksRareExchange = await LooksRareExchange.deploy( - currencyManager.address, - executionManager.address, - royaltyFeeManager.address, - weth.address, - feeRecipient.address - ); - await looksRareExchange.deployed(); - - /** 6. Deploy TransferManager contracts and TransferSelector - */ - const TransferManagerERC721 = await ethers.getContractFactory("TransferManagerERC721"); - const transferManagerERC721 = await TransferManagerERC721.deploy(looksRareExchange.address); - await transferManagerERC721.deployed(); - const TransferManagerERC1155 = await ethers.getContractFactory("TransferManagerERC1155"); - const transferManagerERC1155 = await TransferManagerERC1155.deploy(looksRareExchange.address); - await transferManagerERC1155.deployed(); - const TransferManagerNonCompliantERC721 = await ethers.getContractFactory("TransferManagerNonCompliantERC721"); - const transferManagerNonCompliantERC721 = await TransferManagerNonCompliantERC721.deploy(looksRareExchange.address); - await transferManagerNonCompliantERC721.deployed(); - const TransferSelectorNFT = await ethers.getContractFactory("TransferSelectorNFT"); - const transferSelectorNFT = await TransferSelectorNFT.deploy( - transferManagerERC721.address, - transferManagerERC1155.address - ); - await transferSelectorNFT.deployed(); - - // Set TransferSelectorNFT in LooksRare exchange - await looksRareExchange.connect(admin).updateTransferSelectorNFT(transferSelectorNFT.address); - - /** Return contracts - */ - return [ - weth, - mockERC721, - mockERC1155, - mockUSDT, - mockERC721WithRoyalty, - currencyManager, - executionManager, - transferSelectorNFT, - transferManagerERC721, - transferManagerERC1155, - transferManagerNonCompliantERC721, - looksRareExchange, - strategyStandardSaleForFixedPrice, - strategyAnyItemFromCollectionForFixedPrice, - strategyDutchAuction, - strategyPrivateSale, - strategyAnyItemInASetForFixedPrice, - royaltyFeeRegistry, - royaltyFeeManager, - royaltyFeeSetter, - ]; -} diff --git a/test/token-set-up.ts b/test/token-set-up.ts deleted file mode 100644 index 5199059..0000000 --- a/test/token-set-up.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { constants, Contract } from "ethers"; -import { defaultAbiCoder, parseEther } from "ethers/lib/utils"; - -export async function tokenSetUp( - users: SignerWithAddress[], - weth: Contract, - mockERC721: Contract, - mockERC721WithRoyalty: Contract, - mockERC1155: Contract, - looksRareExchange: Contract, - transferManagerERC721: Contract, - transferManagerERC1155: Contract -): Promise { - for (const user of users) { - // Each user gets 30 WETH - await weth.connect(user).deposit({ value: parseEther("30") }); - - // Set approval for WETH - await weth.connect(user).approve(looksRareExchange.address, constants.MaxUint256); - - // Each user mints 1 ERC721 NFT - await mockERC721.connect(user).mint(user.address); - - // Set approval for all tokens in mock collection to transferManager contract for ERC721 - await mockERC721.connect(user).setApprovalForAll(transferManagerERC721.address, true); - - // Each user mints 1 ERC721WithRoyalty NFT - await mockERC721WithRoyalty.connect(user).mint(user.address); - - // Set approval for all tokens in mock collection to transferManager contract for ERC721WithRoyalty - await mockERC721WithRoyalty.connect(user).setApprovalForAll(transferManagerERC721.address, true); - - // Each user batch mints 2 ERC1155 for tokenIds 1, 2, 3 - await mockERC1155 - .connect(user) - .mintBatch(user.address, ["1", "2", "3"], ["2", "2", "2"], defaultAbiCoder.encode([], [])); - - // Set approval for all tokens in mock collection to transferManager contract for ERC1155 - await mockERC1155.connect(user).setApprovalForAll(transferManagerERC1155.address, true); - } -}