From 76fad968328bd6eb9a78377a94dea713fa9f2cbe Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Thu, 25 May 2023 09:37:37 +0800 Subject: [PATCH 1/7] chore: basic implementation for BLUR sell --- contracts/interfaces/IPoolMarketplace.sol | 94 +++ contracts/interfaces/IPoolParameters.sol | 55 +- .../protocol/libraries/helpers/Errors.sol | 6 +- .../libraries/logic/PoolExtendedLogic.sol | 364 ++++++++- .../libraries/logic/ValidationLogic.sol | 48 +- .../protocol/libraries/types/DataTypes.sol | 36 + contracts/protocol/pool/PoolMarketplace.sol | 50 ++ contracts/protocol/pool/PoolParameters.sol | 52 ++ .../protocol/tokenization/NTokenBAKC.sol | 7 + helpers/types.ts | 6 +- test/_blur_integration_marketplace.spec.ts | 90 ++- .../_blur_sell_integraion_marketplace.spec.ts | 729 ++++++++++++++++++ 12 files changed, 1478 insertions(+), 59 deletions(-) create mode 100644 test/_blur_sell_integraion_marketplace.spec.ts diff --git a/contracts/interfaces/IPoolMarketplace.sol b/contracts/interfaces/IPoolMarketplace.sol index 543955987..e5a958750 100644 --- a/contracts/interfaces/IPoolMarketplace.sol +++ b/contracts/interfaces/IPoolMarketplace.sol @@ -76,6 +76,66 @@ interface IPoolMarketplace { uint256 tokenId ); + /** + * @dev Emitted on initiateAcceptBlurBidsRequest() + * @param initiator The address of initiator of the request + * @param paymentToken The address of paymentToken of the request + * @param bidingPrice The listing price of the request + * @param marketPlaceFee The market place fee taken from bidingPrice + * @param collection the collection address of the erc721 + * @param tokenId the tokenId address of the erc721 + * @param bidOrderHash the biding order hash + **/ + event AcceptBlurBidsRequestInitiated( + address indexed initiator, + address paymentToken, + uint256 bidingPrice, + uint256 marketPlaceFee, + address collection, + uint256 tokenId, + bytes32 bidOrderHash + ); + + /** + * @dev Emitted on fulfillAcceptBlurBidsRequest() + * @param initiator The address of initiator of the request + * @param paymentToken The address of paymentToken of the request + * @param bidingPrice The listing price of the request + * @param marketPlaceFee The market place fee taken from bidingPrice + * @param collection the collection address of the erc721 + * @param tokenId the tokenId address of the erc721 + * @param bidOrderHash the biding order hash + **/ + event AcceptBlurBidsRequestFulfilled( + address indexed initiator, + address paymentToken, + uint256 bidingPrice, + uint256 marketPlaceFee, + address collection, + uint256 tokenId, + bytes32 bidOrderHash + ); + + /** + * @dev Emitted on rejectAcceptBlurBidsRequest() + * @param initiator The address of initiator of the request + * @param paymentToken The address of paymentToken of the request + * @param bidingPrice The listing price of the request + * @param marketPlaceFee The market place fee taken from bidingPrice + * @param collection the collection address of the erc721 + * @param tokenId the tokenId address of the erc721 + * @param bidOrderHash the biding order hash + **/ + event AcceptBlurBidsRequestRejected( + address indexed initiator, + address paymentToken, + uint256 bidingPrice, + uint256 marketPlaceFee, + address collection, + uint256 tokenId, + bytes32 bidOrderHash + ); + /** * @notice Implements the buyWithCredit feature. BuyWithCredit allows users to buy NFT from various NFT marketplaces * including OpenSea, LooksRare, X2Y2 etc. Users can use NFT's credit and will need to pay at most (1 - LTV) * $NFT @@ -179,4 +239,38 @@ interface IPoolMarketplace { function getBlurExchangeRequestStatus( DataTypes.BlurBuyWithCreditRequest calldata request ) external view returns (DataTypes.BlurBuyWithCreditRequestStatus); + + /** + * @notice Initiate accept blur bids for underlying request. + * @dev Only the request initiator can call this function + * @param requests The request array + */ + function initiateAcceptBlurBidsRequest( + DataTypes.AcceptBlurBidsRequest[] calldata requests + ) external payable; + + /** + * @notice Fulfill accept blur bids for underlying request if the blur selling transaction is successes. + * @dev Only keeper can call this function + * @param requests The request array + */ + function fulfillAcceptBlurBidsRequest( + DataTypes.AcceptBlurBidsRequest[] calldata requests + ) external; + + /** + * @notice Reject accept blur bids for underlying request if the blur selling transaction is failed. + * @dev Only keeper can call this function + * @param requests The request array + */ + function rejectAcceptBlurBidsRequest( + DataTypes.AcceptBlurBidsRequest[] calldata requests + ) external; + + /** + * @notice Get a accept blur bids for underlying request status. + */ + function getAcceptBlurBidsRequestStatus( + DataTypes.AcceptBlurBidsRequest calldata request + ) external view returns (DataTypes.AcceptBlurBidsRequestStatus); } diff --git a/contracts/interfaces/IPoolParameters.sol b/contracts/interfaces/IPoolParameters.sol index dd5cbf706..babef95ce 100644 --- a/contracts/interfaces/IPoolParameters.sol +++ b/contracts/interfaces/IPoolParameters.sol @@ -50,6 +50,31 @@ interface IPoolParameters { * @dev Emitted when the blur exchange keeper address update **/ event BlurExchangeKeeperUpdated(address keeper); + /** + * @dev Emitted when the status of accept blur bids enable status update + **/ + event AcceptBlurBidsEnableStatusUpdated(bool isEnable); + + /** + * @dev Emitted when the limit amount of accept blur bids ongoing request update + **/ + event AcceptBlurBidsOngoingRequestLimitUpdated( + uint256 oldValue, + uint256 newValue + ); + + /** + * @dev Emitted when the fee rate of accept blur bids request update + **/ + event AcceptBlurBidsRequestFeeRateUpdated( + uint256 oldValue, + uint256 newValue + ); + + /** + * @dev Emitted when the accept blur bids keeper address update + **/ + event AcceptBlurBidsKeeperUpdated(address keeper); /** * @notice Initializes a reserve, activating it, assigning an xToken and debt tokens and an @@ -243,8 +268,36 @@ interface IPoolParameters { function setBlurExchangeRequestFeeRate(uint16 feeRate) external; /** - * @notice update blur exchange enable status, only pool admin call this function + * @notice update blur exchange keeper, only pool admin call this function * @param keeper The new keeper address **/ function setBlurExchangeKeeper(address keeper) external; + + /** + * @notice enable accept blur bids request, only pool admin call this function + **/ + function enableAcceptBlurBids() external; + + /** + * @notice disable accept blur bids request, only pool admin or emergency admin call this function + **/ + function disableAcceptBlurBids() external; + + /** + * @notice update accept blur bids ongoing request limit amount + * @param limit The new limit amount + **/ + function setAcceptBlurBidsOngoingRequestLimit(uint8 limit) external; + + /** + * @notice update accept blur bids request fee rate + * @param feeRate The new fee rate + **/ + function setAcceptBlurBidsRequestFeeRate(uint16 feeRate) external; + + /** + * @notice update accept blur bids keeper, only pool admin call this function + * @param keeper The new keeper address + **/ + function setAcceptBlurBidsKeeper(address keeper) external; } diff --git a/contracts/protocol/libraries/helpers/Errors.sol b/contracts/protocol/libraries/helpers/Errors.sol index 48a9058b1..48f691b20 100644 --- a/contracts/protocol/libraries/helpers/Errors.sol +++ b/contracts/protocol/libraries/helpers/Errors.sol @@ -136,12 +136,14 @@ library Errors { string public constant CALLER_NOT_INITIATOR = "141"; //The caller of the function is not the request initiator string public constant INVALID_KEEPER_ADDRESS = "142"; //invalid keeper address to receive money string public constant ONGOING_REQUEST_AMOUNT_EXCEEDED = "143"; //ongoing request amount exceeds limit - string public constant BLUR_EXCHANGE_REQUEST_DISABLED = "144"; //blur exchange request disabled + string public constant REQUEST_DISABLED = "144"; //blur exchange request disabled string public constant INVALID_ASSET = "145"; // invalid asset. string public constant INVALID_ETH_VALUE = "146"; //the eth value with the transaction is invalid string public constant INVALID_REQUEST_STATUS = "147"; //The status of the request is invalid for this function string public constant INVALID_PAYMENT_TOKEN = "148"; //the invalid payment token for blur exchange request - string public constant INVALID_LISTING_PRICE = "149"; //the listing price for blur exchange request is invalid + string public constant INVALID_REQUEST_PRICE = "149"; //the listing price for blur exchange request is invalid string public constant CALLER_NOT_KEEPER = "150"; //The caller of the function is not keeper string public constant NTOKEN_NOT_OWNS_UNDERLYING = "151"; //The ntoken does not owns the underlying nft + string public constant EXISTING_APE_STAKING = "152"; // Ape coin staking position existed + string public constant NOT_SAME_NTOKEN_OWNER = "153"; // ntoken have different owner } diff --git a/contracts/protocol/libraries/logic/PoolExtendedLogic.sol b/contracts/protocol/libraries/logic/PoolExtendedLogic.sol index 77afb012c..dfa6955d6 100644 --- a/contracts/protocol/libraries/logic/PoolExtendedLogic.sol +++ b/contracts/protocol/libraries/logic/PoolExtendedLogic.sol @@ -18,6 +18,10 @@ import {IWETH} from "../../../misc/interfaces/IWETH.sol"; import {IPToken} from "../../../interfaces/IPToken.sol"; import {IERC721} from "../../../dependencies/openzeppelin/contracts/IERC721.sol"; import {PercentageMath} from "../../../protocol/libraries/math/PercentageMath.sol"; +import {ApeCoinStaking} from "../../../dependencies/yoga-labs/ApeCoinStaking.sol"; +import {INTokenApeStaking} from "../../../interfaces/INTokenApeStaking.sol"; +import {NTokenBAKC} from "../../tokenization/NTokenBAKC.sol"; +import {XTokenType, IXTokenType} from "../../../interfaces/IXTokenType.sol"; library PoolExtendedLogic { using Math for uint256; @@ -57,6 +61,36 @@ library PoolExtendedLogic { uint256 tokenId ); + event AcceptBlurBidsRequestInitiated( + address indexed initiator, + address paymentToken, + uint256 bidingPrice, + uint256 marketPlaceFee, + address collection, + uint256 tokenId, + bytes32 bidOrderHash + ); + + event AcceptBlurBidsRequestFulfilled( + address indexed initiator, + address paymentToken, + uint256 bidingPrice, + uint256 marketPlaceFee, + address collection, + uint256 tokenId, + bytes32 bidOrderHash + ); + + event AcceptBlurBidsRequestRejected( + address indexed initiator, + address paymentToken, + uint256 bidingPrice, + uint256 marketPlaceFee, + address collection, + uint256 tokenId, + bytes32 bidOrderHash + ); + function executeInitiateBlurExchangeRequest( DataTypes.PoolStorage storage ps, IPoolAddressesProvider poolAddressProvider, @@ -67,7 +101,7 @@ library PoolExtendedLogic { { uint256 ongoingRequestAmount = ps._blurOngoingRequestAmount + requests.length; - ValidationLogic.validateStatusForBlurExchangeRequest( + ValidationLogic.validateStatusForRequest( ps._blurExchangeEnable, keeper, ongoingRequestAmount, @@ -247,7 +281,8 @@ library PoolExtendedLogic { uint256 requestLength = requests.length; address weth = poolAddressProvider.getWETH(); IWETH(weth).deposit{value: msg.value}(); - uint256 remainingETH = msg.value; + address currentOwner; + uint256 totalListingPrice; for (uint256 index = 0; index < requestLength; index++) { DataTypes.BlurBuyWithCreditRequest calldata request = requests[ index @@ -259,11 +294,6 @@ library PoolExtendedLogic { DataTypes.BlurBuyWithCreditRequestStatus.Initiated, Errors.INVALID_REQUEST_STATUS ); - require( - remainingETH >= request.listingPrice, - Errors.INVALID_ETH_VALUE - ); - remainingETH -= request.listingPrice; delete ps._blurExchangeRequestStatus[requestHash]; @@ -271,17 +301,21 @@ library PoolExtendedLogic { request.collection ]; address nTokenAddress = nftReserve.xTokenAddress; - address currentOwner = INToken(nTokenAddress).ownerOf( + + // check if have the same owner + address nTokenOwner = INToken(nTokenAddress).ownerOf( request.tokenId ); - //here we repay and supply weth for currentOwner in case nToken has been liquidated from request initiator - repayAndSupplyForUser( - ps, - weth, - address(this), - currentOwner, - request.listingPrice - ); + if (currentOwner == address(0)) { + currentOwner = nTokenOwner; + } else { + require( + currentOwner == nTokenOwner, + Errors.NOT_SAME_NTOKEN_OWNER + ); + } + + totalListingPrice += request.listingPrice; //burn nToken. burnUserNToken( @@ -304,7 +338,16 @@ library PoolExtendedLogic { request.tokenId ); } - require(remainingETH == 0, Errors.INVALID_ETH_VALUE); + require(msg.value == totalListingPrice, Errors.INVALID_ETH_VALUE); + + //here we repay and supply weth for currentOwner in case nToken has been liquidated from request initiator + repayAndSupplyForUser( + ps, + weth, + address(this), + currentOwner, + totalListingPrice + ); ps._blurOngoingRequestAmount -= requestLength.toUint8(); } @@ -317,6 +360,250 @@ library PoolExtendedLogic { return ps._blurExchangeRequestStatus[requestHash]; } + function executeInitiateAcceptBlurBidsRequest( + DataTypes.PoolStorage storage ps, + IPoolAddressesProvider poolAddressProvider, + DataTypes.AcceptBlurBidsRequest[] calldata requests + ) external { + address keeper = ps._acceptBlurBidsKeeper; + //check and update overall status + { + uint256 ongoingRequestAmount = ps + ._acceptBlurBidsOngoingRequestAmount + requests.length; + ValidationLogic.validateStatusForRequest( + ps._acceptBlurBidsEnable, + keeper, + ongoingRequestAmount, + ps._acceptBlurBidsRequestLimit + ); + ps._acceptBlurBidsOngoingRequestAmount = ongoingRequestAmount + .toUint8(); + } + + // validate user's health factor, if HF drops below 1 before keeper finalize the request, nToken can be liquidated. + address oracle = poolAddressProvider.getPriceOracle(); + ValidationLogic.validateHealthFactor( + ps._reserves, + ps._reservesList, + ps._usersConfig[msg.sender], + msg.sender, + ps._reservesCount, + oracle + ); + + //validate and handle every single request + address weth = poolAddressProvider.getWETH(); + uint256 requestFeeRate = ps._acceptBlurBidsRequestFeeRate; + uint256 totalFee = 0; + for (uint256 index = 0; index < requests.length; index++) { + DataTypes.AcceptBlurBidsRequest calldata request = requests[index]; + bytes32 requestHash = _calculateAcceptBlurBidsRequestHash(request); + totalFee += request.bidingPrice.percentMul(requestFeeRate); + + address nTokenAddress = ps + ._reserves[request.collection] + .xTokenAddress; + + //validate request + ValidationLogic.validateInitiateAcceptBlurBidsRequest( + ps, + nTokenAddress, + request, + requestHash, + weth, + oracle + ); + + //check if any ape coin position exist on the Ape + { + XTokenType tokenType = INToken(nTokenAddress).getXTokenType(); + if (tokenType == XTokenType.NTokenBAYC) { + _ensureApeCoinPositionNotExistedOn( + INTokenApeStaking(nTokenAddress).getApeStaking(), + 1, + request.tokenId + ); + } else if (tokenType == XTokenType.NTokenMAYC) { + _ensureApeCoinPositionNotExistedOn( + INTokenApeStaking(nTokenAddress).getApeStaking(), + 2, + request.tokenId + ); + } else if (tokenType == XTokenType.NTokenBAKC) { + _ensureApeCoinPositionNotExistedOn( + NTokenBAKC(nTokenAddress).getApeStaking(), + 3, + request.tokenId + ); + } + } + + // transfer underlying nft from nToken to keeper + DataTypes.TimeLockParams memory timeLockParams; + INToken(nTokenAddress).transferUnderlyingTo( + keeper, + request.tokenId, + timeLockParams + ); + + // update request status + ps._acceptBlurBidsRequestStatus[requestHash] = DataTypes + .AcceptBlurBidsRequestStatus + .Initiated; + + //emit event + emit AcceptBlurBidsRequestInitiated( + request.initiator, + request.paymentToken, + request.bidingPrice, + request.marketPlaceFee, + request.collection, + request.tokenId, + request.bidOrderHash + ); + } + + require(totalFee == msg.value, Errors.INVALID_ETH_VALUE); + //transfer fee to keeper + if (totalFee > 0) { + Helpers.safeTransferETH(keeper, totalFee); + } + } + + function executeFulfillAcceptBlurBidsRequest( + DataTypes.PoolStorage storage ps, + IPoolAddressesProvider poolAddressProvider, + DataTypes.AcceptBlurBidsRequest[] calldata requests + ) external { + address keeper = ps._acceptBlurBidsKeeper; + require(msg.sender == keeper, Errors.CALLER_NOT_KEEPER); + + uint256 requestLength = requests.length; + uint256 totalWETH = 0; + address currentOwner; + for (uint256 index = 0; index < requestLength; index++) { + DataTypes.AcceptBlurBidsRequest calldata request = requests[index]; + // check request status + bytes32 requestHash = _calculateAcceptBlurBidsRequestHash(request); + require( + ps._acceptBlurBidsRequestStatus[requestHash] == + DataTypes.AcceptBlurBidsRequestStatus.Initiated, + Errors.INVALID_REQUEST_STATUS + ); + + DataTypes.ReserveData storage nftReserve = ps._reserves[ + request.collection + ]; + address nTokenAddress = nftReserve.xTokenAddress; + + // check if have the same owner + address nTokenOwner = INToken(nTokenAddress).ownerOf( + request.tokenId + ); + if (currentOwner == address(0)) { + currentOwner = nTokenOwner; + } else { + require( + currentOwner == nTokenOwner, + Errors.NOT_SAME_NTOKEN_OWNER + ); + } + + // calculate and accumulate weth + totalWETH += (request.bidingPrice - request.marketPlaceFee); + + // update request status + delete ps._blurExchangeRequestStatus[requestHash]; + + //burn ntoken + burnUserNToken( + ps._usersConfig[currentOwner], + request.collection, + nftReserve.id, + nTokenAddress, + request.tokenId, + false, + true, + currentOwner + ); + + //emit event + emit AcceptBlurBidsRequestFulfilled( + request.initiator, + request.paymentToken, + request.bidingPrice, + request.marketPlaceFee, + request.collection, + request.tokenId, + request.bidOrderHash + ); + } + + //supply eth for current ntoken owner + if (totalWETH > 0) { + address weth = poolAddressProvider.getWETH(); + supplyForUser(ps, weth, keeper, currentOwner, totalWETH); + } + + // update ongoing request amount + ps._acceptBlurBidsOngoingRequestAmount -= requestLength.toUint8(); + } + + function executeRejectAcceptBlurBidsRequest( + DataTypes.PoolStorage storage ps, + DataTypes.AcceptBlurBidsRequest[] calldata requests + ) external { + address keeper = ps._acceptBlurBidsKeeper; + require(msg.sender == keeper, Errors.CALLER_NOT_KEEPER); + + uint256 requestLength = requests.length; + for (uint256 index = 0; index < requestLength; index++) { + DataTypes.AcceptBlurBidsRequest calldata request = requests[index]; + // check request status + bytes32 requestHash = _calculateAcceptBlurBidsRequestHash(request); + require( + ps._acceptBlurBidsRequestStatus[requestHash] == + DataTypes.AcceptBlurBidsRequestStatus.Initiated, + Errors.INVALID_REQUEST_STATUS + ); + + // update request status + delete ps._blurExchangeRequestStatus[requestHash]; + + //transfer underlying nft back to nToken + DataTypes.ReserveData storage nftReserve = ps._reserves[ + request.collection + ]; + IERC721(request.collection).safeTransferFrom( + keeper, + nftReserve.xTokenAddress, + request.tokenId + ); + + //emit event + emit AcceptBlurBidsRequestRejected( + request.initiator, + request.paymentToken, + request.bidingPrice, + request.marketPlaceFee, + request.collection, + request.tokenId, + request.bidOrderHash + ); + } + + // update ongoing request amount + ps._acceptBlurBidsOngoingRequestAmount -= requestLength.toUint8(); + } + + function executeGetAcceptBlurBidsRequestStatus( + DataTypes.PoolStorage storage ps, + DataTypes.AcceptBlurBidsRequest calldata request + ) external view returns (DataTypes.AcceptBlurBidsRequestStatus) { + bytes32 requestHash = _calculateAcceptBlurBidsRequestHash(request); + return ps._acceptBlurBidsRequestStatus[requestHash]; + } + function _calculateBlurExchangeRequestHash( DataTypes.BlurBuyWithCreditRequest calldata request ) internal pure returns (bytes32) { @@ -333,6 +620,23 @@ library PoolExtendedLogic { ); } + function _calculateAcceptBlurBidsRequestHash( + DataTypes.AcceptBlurBidsRequest calldata request + ) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + request.initiator, + request.paymentToken, + request.bidingPrice, + request.marketPlaceFee, + request.collection, + request.tokenId, + request.bidOrderHash + ) + ); + } + function repayAndSupplyForUser( DataTypes.PoolStorage storage ps, address asset, @@ -428,15 +732,27 @@ library PoolExtendedLogic { tokenIds[0] = tokenId; // no time lock needed here DataTypes.TimeLockParams memory timeLockParams; - (, uint64 collateralizedBalance) = INToken(nTokenAddress).burn( - user, - releaseUnderlying ? user : nTokenAddress, - tokenIds, - timeLockParams - ); - if (collateralizedBalance == 0) { + ( + uint64 oldCollateralizedBalance, + uint64 collateralizedBalance + ) = INToken(nTokenAddress).burn( + user, + releaseUnderlying ? user : nTokenAddress, + tokenIds, + timeLockParams + ); + if (oldCollateralizedBalance > 0 && collateralizedBalance == 0) { userConfig.setUsingAsCollateral(reserveIndex, false); emit ReserveUsedAsCollateralDisabled(asset, user); } } + + function _ensureApeCoinPositionNotExistedOn( + ApeCoinStaking apeStaking, + uint256 poolId, + uint256 tokenId + ) internal view { + (uint256 stakedAmount, ) = apeStaking.nftPosition(poolId, tokenId); + require(stakedAmount == 0, Errors.EXISTING_APE_STAKING); + } } diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 6387c5103..15fd94aeb 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -55,13 +55,55 @@ library ValidationLogic { */ uint256 public constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1e18; - function validateStatusForBlurExchangeRequest( + function validateInitiateAcceptBlurBidsRequest( + DataTypes.PoolStorage storage ps, + address nTokenAddress, + DataTypes.AcceptBlurBidsRequest calldata request, + bytes32 requestHash, + address weth, + address oracle + ) internal view { + require( + ps._acceptBlurBidsRequestStatus[requestHash] == + DataTypes.AcceptBlurBidsRequestStatus.Default, + Errors.INVALID_REQUEST_STATUS + ); + require(msg.sender == request.initiator, Errors.CALLER_NOT_INITIATOR); + require(request.paymentToken == weth, Errors.INVALID_PAYMENT_TOKEN); + uint256 floorPrice = IPriceOracleGetter(oracle).getAssetPrice( + request.collection + ); + uint256 collateralPrice = Helpers.getTraitBoostedTokenPrice( + nTokenAddress, + floorPrice, + request.tokenId + ); + DataTypes.ReserveConfigurationMap memory nftReserveConfiguration = ps + ._reserves[request.collection] + .configuration; + (, uint256 nftLiquidationThreshold, , , ) = nftReserveConfiguration + .getParams(); + DataTypes.ReserveConfigurationMap memory wethReserveConfiguration = ps + ._reserves[weth] + .configuration; + (, uint256 wethLiquidationThreshold, , , ) = wethReserveConfiguration + .getParams(); + uint256 userReceivedCurrency = request.bidingPrice - + request.marketPlaceFee; + require( + userReceivedCurrency.percentMul(wethLiquidationThreshold) >= + collateralPrice.percentMul(nftLiquidationThreshold), + Errors.INVALID_REQUEST_PRICE + ); + } + + function validateStatusForRequest( bool isEnable, address keeper, uint256 ongoingRequestAmount, uint256 ongoingRequestLimit ) internal pure { - require(isEnable, Errors.BLUR_EXCHANGE_REQUEST_DISABLED); + require(isEnable, Errors.REQUEST_DISABLED); require(keeper != address(0), Errors.INVALID_KEEPER_ADDRESS); require( ongoingRequestAmount <= ongoingRequestLimit, @@ -102,7 +144,7 @@ library ValidationLogic { // ensure user can't borrow/withdraw with the new mint nToken require( request.listingPrice >= collateralPrice, - Errors.INVALID_LISTING_PRICE + Errors.INVALID_REQUEST_PRICE ); } diff --git a/contracts/protocol/libraries/types/DataTypes.sol b/contracts/protocol/libraries/types/DataTypes.sol index d22feb07a..7bbae02fd 100644 --- a/contracts/protocol/libraries/types/DataTypes.sol +++ b/contracts/protocol/libraries/types/DataTypes.sol @@ -415,6 +415,18 @@ library DataTypes { address _blurExchangeKeeper; // Map of BuyWithCreditRequest status mapping(bytes32 => BlurBuyWithCreditRequestStatus) _blurExchangeRequestStatus; + //identified if accept blur bid is enabled + bool _acceptBlurBidsEnable; + // max amount of accept blur bid request, 0 means no limit. + uint8 _acceptBlurBidsRequestLimit; + // the amount of ongoing accept blur bid request + uint8 _acceptBlurBidsOngoingRequestAmount; + // accept blur bid request fee rate + uint16 _acceptBlurBidsRequestFeeRate; + //accept blur bid keeper + address _acceptBlurBidsKeeper; + // Map of AcceptBlurBidsRequest status + mapping(bytes32 => AcceptBlurBidsRequestStatus) _acceptBlurBidsRequestStatus; } struct ReserveConfigData { @@ -467,4 +479,28 @@ library DataTypes { // nft token id for the listing order uint256 tokenId; } + + enum AcceptBlurBidsRequestStatus { + //default status for value 0 + Default, + //initiated status + Initiated + } + + struct AcceptBlurBidsRequest { + // request initiator + address initiator; + // currency token. currently should always be weth + address paymentToken; + // cash amount from user wallet + uint256 bidingPrice; + // market place fee, taken from bidingPrice + uint256 marketPlaceFee; + // nft token address for the listing order + address collection; + // nft token id for the listing order + uint256 tokenId; + // bid order hash + bytes32 bidOrderHash; + } } diff --git a/contracts/protocol/pool/PoolMarketplace.sol b/contracts/protocol/pool/PoolMarketplace.sol index b76a7145a..1781da900 100644 --- a/contracts/protocol/pool/PoolMarketplace.sol +++ b/contracts/protocol/pool/PoolMarketplace.sol @@ -197,4 +197,54 @@ contract PoolMarketplace is return PoolExtendedLogic.executeGetBlurExchangeRequestStatus(ps, request); } + + /// @inheritdoc IPoolMarketplace + function initiateAcceptBlurBidsRequest( + DataTypes.AcceptBlurBidsRequest[] calldata requests + ) external payable override { + DataTypes.PoolStorage storage ps = poolStorage(); + PoolExtendedLogic.executeInitiateAcceptBlurBidsRequest( + ps, + ADDRESSES_PROVIDER, + requests + ); + } + + /// @inheritdoc IPoolMarketplace + function fulfillAcceptBlurBidsRequest( + DataTypes.AcceptBlurBidsRequest[] calldata requests + ) external virtual override { + DataTypes.PoolStorage storage ps = poolStorage(); + PoolExtendedLogic.executeFulfillAcceptBlurBidsRequest( + ps, + ADDRESSES_PROVIDER, + requests + ); + } + + /// @inheritdoc IPoolMarketplace + function rejectAcceptBlurBidsRequest( + DataTypes.AcceptBlurBidsRequest[] calldata requests + ) external override { + DataTypes.PoolStorage storage ps = poolStorage(); + PoolExtendedLogic.executeRejectAcceptBlurBidsRequest(ps, requests); + } + + /// @inheritdoc IPoolMarketplace + function getAcceptBlurBidsRequestStatus( + DataTypes.AcceptBlurBidsRequest calldata request + ) + external + view + virtual + override + returns (DataTypes.AcceptBlurBidsRequestStatus) + { + DataTypes.PoolStorage storage ps = poolStorage(); + return + PoolExtendedLogic.executeGetAcceptBlurBidsRequestStatus( + ps, + request + ); + } } diff --git a/contracts/protocol/pool/PoolParameters.sol b/contracts/protocol/pool/PoolParameters.sol index 9667172df..c5fbaad5f 100644 --- a/contracts/protocol/pool/PoolParameters.sol +++ b/contracts/protocol/pool/PoolParameters.sol @@ -429,4 +429,56 @@ contract PoolParameters is ps._blurExchangeKeeper = keeper; emit BlurExchangeKeeperUpdated(keeper); } + + /// @inheritdoc IPoolParameters + function enableAcceptBlurBids() external onlyPoolAdmin { + DataTypes.PoolStorage storage ps = poolStorage(); + if (!ps._acceptBlurBidsEnable) { + ps._acceptBlurBidsEnable = true; + emit AcceptBlurBidsEnableStatusUpdated(true); + } + } + + /// @inheritdoc IPoolParameters + function disableAcceptBlurBids() external onlyEmergencyOrPoolAdmin { + DataTypes.PoolStorage storage ps = poolStorage(); + if (ps._acceptBlurBidsEnable) { + ps._acceptBlurBidsEnable = false; + emit AcceptBlurBidsEnableStatusUpdated(false); + } + } + + /// @inheritdoc IPoolParameters + function setAcceptBlurBidsOngoingRequestLimit(uint8 limit) + external + onlyPoolAdmin + { + DataTypes.PoolStorage storage ps = poolStorage(); + uint8 oldValue = ps._acceptBlurBidsRequestLimit; + if (oldValue != limit) { + ps._acceptBlurBidsRequestLimit = limit; + emit AcceptBlurBidsOngoingRequestLimitUpdated(oldValue, limit); + } + } + + /// @inheritdoc IPoolParameters + function setAcceptBlurBidsRequestFeeRate(uint16 feeRate) + external + onlyPoolAdmin + { + DataTypes.PoolStorage storage ps = poolStorage(); + uint16 oldValue = ps._acceptBlurBidsRequestFeeRate; + if (oldValue != feeRate) { + ps._acceptBlurBidsRequestFeeRate = feeRate; + emit AcceptBlurBidsRequestFeeRateUpdated(oldValue, feeRate); + } + } + + /// @inheritdoc IPoolParameters + function setAcceptBlurBidsKeeper(address keeper) external onlyPoolAdmin { + require(keeper != address(0), Errors.ZERO_ADDRESS_NOT_VALID); + DataTypes.PoolStorage storage ps = poolStorage(); + ps._acceptBlurBidsKeeper = keeper; + emit AcceptBlurBidsKeeperUpdated(keeper); + } } diff --git a/contracts/protocol/tokenization/NTokenBAKC.sol b/contracts/protocol/tokenization/NTokenBAKC.sol index 3252e519f..c913b59d6 100644 --- a/contracts/protocol/tokenization/NTokenBAKC.sol +++ b/contracts/protocol/tokenization/NTokenBAKC.sol @@ -71,6 +71,13 @@ contract NTokenBAKC is NToken { IERC721(underlyingAsset).setApprovalForAll(address(POOL), true); } + /** + * @notice Returns the address of ApeCoinStaking contract address. + **/ + function getApeStaking() external view returns (ApeCoinStaking) { + return _apeCoinStaking; + } + function _transfer( address from, address to, diff --git a/helpers/types.ts b/helpers/types.ts index 282f79f7c..919557079 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -432,14 +432,16 @@ export enum ProtocolErrors { CALLER_NOT_INITIATOR = "141", //The caller of the function is not the request initiator ONGOING_REQUEST_AMOUNT_EXCEEDED = "143", //ongoing request amount exceeds limit - BLUR_EXCHANGE_REQUEST_DISABLED = "144", //blur exchange request disabled + REQUEST_DISABLED = "144", //blur exchange request disabled INVALID_ASSET = "145", // invalid asset. INVALID_ETH_VALUE = "146", //The status of the request is invalid for this function INVALID_REQUEST_STATUS = "147", //the eth value with the transaction is invalid INVALID_PAYMENT_TOKEN = "148", //the invalid payment token for blur exchange request - INVALID_LISTING_PRICE = "149", //the listing price for blur exchange request is invalid + INVALID_REQUEST_PRICE = "149", //the listing price for blur exchange request is invalid CALLER_NOT_KEEPER = "150", //The caller of the function is not keeper NTOKEN_NOT_OWNS_UNDERLYING = "151", //The ntoken does not owns the underlying nft + EXISTING_APE_STAKING = "152", // Ape coin staking position existed + NOT_SAME_NTOKEN_OWNER = "153", // ntoken have different owner } export type tEthereumAddress = string; diff --git a/test/_blur_integration_marketplace.spec.ts b/test/_blur_integration_marketplace.spec.ts index c6e7ea585..48d7feec1 100644 --- a/test/_blur_integration_marketplace.spec.ts +++ b/test/_blur_integration_marketplace.spec.ts @@ -19,7 +19,7 @@ import {zeroAddress} from "ethereumjs-util"; import {BigNumber} from "ethers"; import {convertToCurrencyDecimals} from "../helpers/contracts-helpers"; -describe("BLUR integration tests", () => { +describe("BLUR Buy Integration Tests", () => { let ETHExchangeRequest; let WETHExchangeRequest; let wethDebtToken; @@ -475,7 +475,7 @@ describe("BLUR integration tests", () => { pool.connect(user1.signer).initiateBlurExchangeRequest([invalidRequest], { value: parseEther("80"), }) - ).to.be.revertedWith(ProtocolErrors.INVALID_LISTING_PRICE); + ).to.be.revertedWith(ProtocolErrors.INVALID_REQUEST_PRICE); }); it("listing price must > trait boosted price * ls when initiate request", async () => { @@ -506,7 +506,7 @@ describe("BLUR integration tests", () => { pool.connect(user1.signer).initiateBlurExchangeRequest([invalidRequest], { value: parseEther("110"), }) - ).to.be.revertedWith(ProtocolErrors.INVALID_LISTING_PRICE); + ).to.be.revertedWith(ProtocolErrors.INVALID_REQUEST_PRICE); }); it("ongoing request count must <= limit", async () => { @@ -617,7 +617,7 @@ describe("BLUR integration tests", () => { .initiateBlurExchangeRequest([ETHExchangeRequest], { value: parseEther("80"), }) - ).to.be.revertedWith(ProtocolErrors.BLUR_EXCHANGE_REQUEST_DISABLED); + ).to.be.revertedWith(ProtocolErrors.REQUEST_DISABLED); }); it("should repay and supply for new owner when reject request if the nToken is liquidated", async () => { @@ -681,27 +681,63 @@ describe("BLUR integration tests", () => { almostEqual(afterPWethBalance.sub(beforePWethBalance), parseEther("110")); }); - // it("initiate request failed when asset liquidation threshold is 0", async () => { - // const { - // pool, - // users: [user1], - // poolAdmin, - // bayc, - // configurator, - // } = await loadFixture(fixture); - // - // await waitForTx( - // await configurator - // .connect(poolAdmin.signer) - // .configureReserveAsCollateral(bayc.address, 0, 0, 0) - // ); - // - // await expect( - // pool - // .connect(user1.signer) - // .initiateBlurExchangeRequest([ETHExchangeRequest], { - // value: parseEther("80"), - // }) - // ).to.be.revertedWith(ProtocolErrors.INVALID_ASSET); - // }); + it("reject requests failed if ntokens have different owner", async () => { + const { + pool, + bayc, + mayc, + weth, + users: [user1, user2, user3], + } = await loadFixture(fixture); + + const ETHExchangeRequest1 = { + initiator: user1.address, + paymentToken: zeroAddress(), + listingPrice: parseEther("60"), + borrowAmount: parseEther("15"), + collection: mayc.address, + tokenId: 0, + }; + + await waitForTx( + await pool + .connect(user1.signer) + .initiateBlurExchangeRequest( + [ETHExchangeRequest, ETHExchangeRequest1], + { + value: parseEther("125"), + } + ) + ); + + await changePriceAndValidate(bayc, "10"); + + // start auction + await waitForTx( + await pool + .connect(user3.signer) + .startAuction(user1.address, bayc.address, 0) + ); + + await waitForTx( + await pool + .connect(user3.signer) + .liquidateERC721( + bayc.address, + user1.address, + 0, + await convertToCurrencyDecimals(weth.address, "100"), + true, + {gasLimit: 5000000, value: parseEther("100")} + ) + ); + + await expect( + pool + .connect(user2.signer) + .rejectBlurExchangeRequest([ETHExchangeRequest, ETHExchangeRequest1], { + value: parseEther("185"), + }) + ).to.be.revertedWith(ProtocolErrors.NOT_SAME_NTOKEN_OWNER); + }); }); diff --git a/test/_blur_sell_integraion_marketplace.spec.ts b/test/_blur_sell_integraion_marketplace.spec.ts new file mode 100644 index 000000000..0b7c2728d --- /dev/null +++ b/test/_blur_sell_integraion_marketplace.spec.ts @@ -0,0 +1,729 @@ +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {expect} from "chai"; +import {MAX_UINT_AMOUNT, WAD} from "../helpers/constants"; +import {waitForTx} from "../helpers/misc-utils"; +import {ProtocolErrors} from "../helpers/types"; +import {testEnvFixture} from "./helpers/setup-env"; +import { + changePriceAndValidate, + mintAndValidate, + supplyAndValidate, +} from "./helpers/validated-steps"; +import {parseEther, solidityKeccak256} from "ethers/lib/utils"; +import {almostEqual} from "./helpers/uniswapv3-helper"; +import {BigNumber} from "ethers"; +import {convertToCurrencyDecimals} from "../helpers/contracts-helpers"; + +describe("BLUR Sell Integration Tests", () => { + let AcceptBaycBidsRequest; + let AcceptMaycBidsRequest; + + const fixture = async () => { + const testEnv = await loadFixture(testEnvFixture); + const { + weth, + bayc, + mayc, + pool, + users: [user1, user2, user3], + poolAdmin, + } = testEnv; + + await waitForTx( + await pool.connect(poolAdmin.signer).enableAcceptBlurBids() + ); + + await waitForTx( + await pool + .connect(poolAdmin.signer) + .setAcceptBlurBidsOngoingRequestLimit(2) + ); + + await waitForTx( + await pool + .connect(poolAdmin.signer) + .setAcceptBlurBidsKeeper(user2.address) + ); + + await supplyAndValidate(bayc, "1", user1, true); + await supplyAndValidate(mayc, "1", user1, true); + await mintAndValidate(weth, parseEther("200").toString(), user2); + + await supplyAndValidate(weth, parseEther("100").toString(), user3, true); + + await waitForTx( + await weth.connect(user2.signer).approve(pool.address, MAX_UINT_AMOUNT) + ); + await waitForTx( + await bayc.connect(user2.signer).setApprovalForAll(pool.address, true) + ); + await waitForTx( + await mayc.connect(user2.signer).setApprovalForAll(pool.address, true) + ); + + AcceptBaycBidsRequest = { + initiator: user1.address, + paymentToken: weth.address, + bidingPrice: parseEther("110"), + marketPlaceFee: parseEther("1"), + collection: bayc.address, + tokenId: 0, + bidOrderHash: solidityKeccak256(["uint256"], [0]), + }; + AcceptMaycBidsRequest = { + initiator: user1.address, + paymentToken: weth.address, + bidingPrice: parseEther("60"), + marketPlaceFee: parseEther("1"), + collection: mayc.address, + tokenId: 0, + bidOrderHash: solidityKeccak256(["uint256"], [0]), + }; + + return testEnv; + }; + + it("weth request can be initiated and fulfilled", async () => { + const { + pool, + users: [user1, user2], + pWETH, + bayc, + mayc, + nBAYC, + nMAYC, + poolAdmin, + } = await loadFixture(fixture); + + await waitForTx( + await pool.connect(poolAdmin.signer).setAcceptBlurBidsRequestFeeRate(1000) + ); + + expect(await bayc.balanceOf(user2.address)).to.be.eq(0); + expect(await bayc.balanceOf(nBAYC.address)).to.be.eq(1); + expect(await mayc.balanceOf(user2.address)).to.be.eq(0); + expect(await mayc.balanceOf(nMAYC.address)).to.be.eq(1); + + await waitForTx( + await pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest( + [AcceptBaycBidsRequest, AcceptMaycBidsRequest], + { + value: parseEther("17"), + } + ) + ); + + expect(await bayc.balanceOf(user2.address)).to.be.eq(1); + expect(await bayc.balanceOf(nBAYC.address)).to.be.eq(0); + expect(await mayc.balanceOf(user2.address)).to.be.eq(1); + expect(await mayc.balanceOf(nMAYC.address)).to.be.eq(0); + + expect(await pWETH.balanceOf(user1.address)).to.be.eq(0); + + await waitForTx( + await pool + .connect(user2.signer) + .fulfillAcceptBlurBidsRequest([ + AcceptBaycBidsRequest, + AcceptMaycBidsRequest, + ]) + ); + + almostEqual(await pWETH.balanceOf(user1.address), parseEther("168")); + }); + + it("weth request can be initiated and rejected", async () => { + const { + pool, + users: [user1, user2], + bayc, + mayc, + nBAYC, + nMAYC, + poolAdmin, + } = await loadFixture(fixture); + + await waitForTx( + await pool.connect(poolAdmin.signer).setAcceptBlurBidsRequestFeeRate(1000) + ); + + await waitForTx( + await pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest( + [AcceptBaycBidsRequest, AcceptMaycBidsRequest], + { + value: parseEther("17"), + } + ) + ); + + expect(await bayc.balanceOf(user2.address)).to.be.eq(1); + expect(await bayc.balanceOf(nBAYC.address)).to.be.eq(0); + expect(await mayc.balanceOf(user2.address)).to.be.eq(1); + expect(await mayc.balanceOf(nMAYC.address)).to.be.eq(0); + + await waitForTx( + await pool + .connect(user2.signer) + .rejectAcceptBlurBidsRequest([ + AcceptBaycBidsRequest, + AcceptMaycBidsRequest, + ]) + ); + + expect(await bayc.balanceOf(user2.address)).to.be.eq(0); + expect(await bayc.balanceOf(nBAYC.address)).to.be.eq(1); + expect(await mayc.balanceOf(user2.address)).to.be.eq(0); + expect(await mayc.balanceOf(nMAYC.address)).to.be.eq(1); + }); + + it("invalid payment token request can not be initiated", async () => { + const { + pool, + mayc, + usdt, + users: [user1], + } = await loadFixture(fixture); + + const InvalidRequest = { + initiator: user1.address, + paymentToken: usdt.address, + bidingPrice: parseEther("60"), + marketPlaceFee: parseEther("1"), + collection: mayc.address, + tokenId: 0, + bidOrderHash: solidityKeccak256(["uint256"], [0]), + }; + + await expect( + pool.connect(user1.signer).initiateAcceptBlurBidsRequest([InvalidRequest]) + ).to.be.revertedWith(ProtocolErrors.INVALID_PAYMENT_TOKEN); + }); + + it("only pool admin can enable/disable acdept blur bids", async () => { + const { + pool, + users: [user1], + poolAdmin, + } = await loadFixture(fixture); + await expect( + pool.connect(user1.signer).enableAcceptBlurBids() + ).to.be.revertedWith(ProtocolErrors.CALLER_NOT_POOL_ADMIN); + + await waitForTx( + await pool.connect(poolAdmin.signer).enableAcceptBlurBids() + ); + + await expect( + pool.connect(user1.signer).disableAcceptBlurBids() + ).to.be.revertedWith(ProtocolErrors.CALLER_NOT_POOL_OR_EMERGENCY_ADMIN); + + await waitForTx( + await pool.connect(poolAdmin.signer).disableAcceptBlurBids() + ); + }); + + it("only pool admin can update request limit", async () => { + const { + pool, + users: [user1], + poolAdmin, + } = await loadFixture(fixture); + await expect( + pool.connect(user1.signer).setAcceptBlurBidsOngoingRequestLimit(5) + ).to.be.revertedWith(ProtocolErrors.CALLER_NOT_POOL_ADMIN); + + await waitForTx( + await pool + .connect(poolAdmin.signer) + .setAcceptBlurBidsOngoingRequestLimit(5) + ); + }); + + it("only pool admin can update request fee rate", async () => { + const { + pool, + users: [user1], + poolAdmin, + } = await loadFixture(fixture); + await expect( + pool.connect(user1.signer).setAcceptBlurBidsRequestFeeRate(100) + ).to.be.revertedWith(ProtocolErrors.CALLER_NOT_POOL_ADMIN); + + await waitForTx( + await pool.connect(poolAdmin.signer).setAcceptBlurBidsRequestFeeRate(100) + ); + }); + + it("only pool admin can set blur exchange keeper", async () => { + const { + pool, + users: [user1, user2], + poolAdmin, + } = await loadFixture(fixture); + + await expect( + pool.connect(user1.signer).setAcceptBlurBidsKeeper(user2.address) + ).to.be.revertedWith(ProtocolErrors.CALLER_NOT_POOL_ADMIN); + + await waitForTx( + await pool + .connect(poolAdmin.signer) + .setAcceptBlurBidsKeeper(user2.address) + ); + }); + + it("only request initiator can initiate the request", async () => { + const { + pool, + users: [user1, user2], + } = await loadFixture(fixture); + + await expect( + pool + .connect(user2.signer) + .initiateAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + ).to.be.revertedWith(ProtocolErrors.CALLER_NOT_INITIATOR); + + await waitForTx( + await pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + ); + }); + + it("only keeper can fulfill the request", async () => { + const { + pool, + users: [user1, , user3], + } = await loadFixture(fixture); + + await waitForTx( + await pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + ); + + await expect( + pool + .connect(user3.signer) + .fulfillAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + ).to.be.revertedWith(ProtocolErrors.CALLER_NOT_KEEPER); + }); + + it("only keeper can reject the request", async () => { + const { + pool, + users: [user1, , user3], + } = await loadFixture(fixture); + + await waitForTx( + await pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + ); + + await expect( + pool + .connect(user3.signer) + .rejectAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + ).to.be.revertedWith(ProtocolErrors.CALLER_NOT_KEEPER); + }); + + it("user can't transfer nToken before request is fulfilled", async () => { + const { + pool, + nBAYC, + users: [user1, user2], + } = await loadFixture(fixture); + + await waitForTx( + await pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + ); + + await expect( + nBAYC.connect(user1.signer).transferFrom(user1.address, user2.address, 0) + ).to.be.revertedWith(ProtocolErrors.NTOKEN_NOT_OWNS_UNDERLYING); + }); + + it("user can't borrowApeAndStake before request is fulfilled", async () => { + const { + pool, + users: [user1, user2], + bayc, + bakc, + ape, + } = await loadFixture(fixture); + + await waitForTx( + await pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + ); + + await supplyAndValidate(ape, "200000", user2, true); + + const amount = await convertToCurrencyDecimals(ape.address, "10000"); + await expect( + pool.connect(user1.signer).borrowApeAndStake( + { + nftAsset: bayc.address, + borrowAsset: ape.address, + borrowAmount: amount, + cashAmount: 0, + }, + [{tokenId: 0, amount: amount}], + [] + ) + ).to.be.revertedWith(ProtocolErrors.NTOKEN_NOT_OWNS_UNDERLYING); + + await waitForTx(await bakc["mint(uint256,address)"]("2", user1.address)); + await waitForTx( + await bakc.connect(user1.signer).setApprovalForAll(pool.address, true) + ); + await expect( + pool.connect(user1.signer).borrowApeAndStake( + { + nftAsset: bayc.address, + borrowAsset: ape.address, + borrowAmount: amount, + cashAmount: 0, + }, + [], + [{mainTokenId: 0, bakcTokenId: 0, amount: amount}] + ) + ).to.be.revertedWith(ProtocolErrors.NTOKEN_NOT_OWNS_UNDERLYING); + }); + + it("user can't initiate request if ape or bakc have staking position", async () => { + const { + pool, + users: [user1], + bayc, + bakc, + ape, + weth, + } = await loadFixture(fixture); + + await supplyAndValidate(bakc, "1", user1, true); + await mintAndValidate(ape, "20000", user1); + await waitForTx( + await ape.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT) + ); + + await waitForTx( + await pool.connect(user1.signer).borrowApeAndStake( + { + nftAsset: bayc.address, + borrowAsset: ape.address, + borrowAmount: 0, + cashAmount: parseEther("20000"), + }, + [{tokenId: 0, amount: parseEther("10000")}], + [{mainTokenId: 0, bakcTokenId: 0, amount: parseEther("10000")}] + ) + ); + + await expect( + pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + ).to.be.revertedWith(ProtocolErrors.EXISTING_APE_STAKING); + + const AcceptBakcBidsRequest = { + initiator: user1.address, + paymentToken: weth.address, + bidingPrice: parseEther("10"), + marketPlaceFee: parseEther("1"), + collection: bakc.address, + tokenId: 0, + bidOrderHash: solidityKeccak256(["uint256"], [0]), + }; + + await expect( + pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest([AcceptBakcBidsRequest]) + ).to.be.revertedWith(ProtocolErrors.EXISTING_APE_STAKING); + }); + + it("biding price * ls must > floor price * ls when initiate request", async () => { + const { + pool, + weth, + bayc, + users: [user1], + } = await loadFixture(fixture); + + const invalidRequest = { + initiator: user1.address, + paymentToken: weth.address, + bidingPrice: parseEther("50"), + marketPlaceFee: parseEther("1"), + collection: bayc.address, + tokenId: 0, + bidOrderHash: solidityKeccak256(["uint256"], [0]), + }; + + await expect( + pool.connect(user1.signer).initiateAcceptBlurBidsRequest([invalidRequest]) + ).to.be.revertedWith(ProtocolErrors.INVALID_REQUEST_PRICE); + }); + + it("biding price * ls must > trait boosted price * ls when initiate request", async () => { + const { + pool, + weth, + bayc, + nBAYC, + poolAdmin, + users: [user1], + } = await loadFixture(fixture); + + await waitForTx( + await nBAYC + .connect(poolAdmin.signer) + .setTraitsMultipliers(["0"], [BigNumber.from(WAD).mul(2)]) + ); + + const invalidRequest = { + initiator: user1.address, + paymentToken: weth.address, + bidingPrice: parseEther("110"), + marketPlaceFee: parseEther("1"), + collection: bayc.address, + tokenId: 0, + bidOrderHash: solidityKeccak256(["uint256"], [0]), + }; + + await expect( + pool.connect(user1.signer).initiateAcceptBlurBidsRequest([invalidRequest]) + ).to.be.revertedWith(ProtocolErrors.INVALID_REQUEST_PRICE); + }); + + it("ongoing request count must <= limit", async () => { + const { + pool, + users: [user1], + poolAdmin, + } = await loadFixture(fixture); + + await waitForTx( + await pool + .connect(poolAdmin.signer) + .setAcceptBlurBidsOngoingRequestLimit(1) + ); + + await waitForTx( + await pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + ); + + await expect( + pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest([AcceptMaycBidsRequest]) + ).to.be.revertedWith(ProtocolErrors.ONGOING_REQUEST_AMOUNT_EXCEEDED); + }); + + it("eth request reverted when transaction value is not equal with cash value", async () => { + const { + pool, + users: [user1], + } = await loadFixture(fixture); + + await expect( + pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest([AcceptBaycBidsRequest], { + value: parseEther("100"), + }) + ).to.be.revertedWith(ProtocolErrors.INVALID_ETH_VALUE); + }); + + it("only default status request can be initiated", async () => { + const { + pool, + users: [user1], + } = await loadFixture(fixture); + + await waitForTx( + await pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + ); + await expect( + pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + ).to.be.revertedWith(ProtocolErrors.INVALID_REQUEST_STATUS); + }); + + it("only initiated status request can be fulfilled", async () => { + const { + pool, + users: [, user2], + } = await loadFixture(fixture); + + await expect( + pool + .connect(user2.signer) + .fulfillAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + ).to.be.revertedWith(ProtocolErrors.INVALID_REQUEST_STATUS); + }); + + it("only initiated status request can be rejected", async () => { + const { + pool, + users: [, user2], + } = await loadFixture(fixture); + + await expect( + pool + .connect(user2.signer) + .rejectAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + ).to.be.revertedWith(ProtocolErrors.INVALID_REQUEST_STATUS); + }); + + it("initiate request failed when accept blur bids request disabled", async () => { + const { + pool, + users: [user1], + poolAdmin, + } = await loadFixture(fixture); + + await waitForTx( + await pool.connect(poolAdmin.signer).disableAcceptBlurBids() + ); + + await expect( + pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + ).to.be.revertedWith(ProtocolErrors.REQUEST_DISABLED); + }); + + it("should supply for new owner when fulfill request if the nToken is liquidated", async () => { + const { + pool, + bayc, + nBAYC, + weth, + pWETH, + users: [user1, user2, user3], + } = await loadFixture(fixture); + + await waitForTx( + await pool + .connect(user1.signer) + .borrow(weth.address, parseEther("50"), 0, user1.address) + ); + + await waitForTx( + await pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + ); + + await changePriceAndValidate(bayc, "10"); + + // start auction + await waitForTx( + await pool + .connect(user3.signer) + .startAuction(user1.address, bayc.address, 0) + ); + + expect(await nBAYC.balanceOf(user1.address)).to.be.eq(1); + expect(await nBAYC.balanceOf(user3.address)).to.be.eq(0); + + await waitForTx( + await pool + .connect(user3.signer) + .liquidateERC721( + bayc.address, + user1.address, + 0, + await convertToCurrencyDecimals(weth.address, "100"), + true, + {gasLimit: 5000000, value: parseEther("100")} + ) + ); + + expect(await nBAYC.balanceOf(user1.address)).to.be.eq(0); + expect(await nBAYC.balanceOf(user3.address)).to.be.eq(1); + const beforePWethBalance = await pWETH.balanceOf(user3.address); + + await waitForTx( + await pool + .connect(user2.signer) + .fulfillAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + ); + + expect(await nBAYC.balanceOf(user1.address)).to.be.eq(0); + expect(await nBAYC.balanceOf(user3.address)).to.be.eq(0); + const afterPWethBalance = await pWETH.balanceOf(user3.address); + almostEqual(afterPWethBalance.sub(beforePWethBalance), parseEther("109")); + }); + + it("fulfill requests failed if ntokens have different owner", async () => { + const { + pool, + bayc, + weth, + users: [user1, user2, user3], + } = await loadFixture(fixture); + + await waitForTx( + await pool + .connect(user1.signer) + .borrow(weth.address, parseEther("50"), 0, user1.address) + ); + + await waitForTx( + await pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest([ + AcceptBaycBidsRequest, + AcceptMaycBidsRequest, + ]) + ); + + await changePriceAndValidate(bayc, "10"); + + // start auction + await waitForTx( + await pool + .connect(user3.signer) + .startAuction(user1.address, bayc.address, 0) + ); + + await waitForTx( + await pool + .connect(user3.signer) + .liquidateERC721( + bayc.address, + user1.address, + 0, + await convertToCurrencyDecimals(weth.address, "100"), + true, + {gasLimit: 5000000, value: parseEther("100")} + ) + ); + + await expect( + pool + .connect(user2.signer) + .fulfillAcceptBlurBidsRequest([ + AcceptBaycBidsRequest, + AcceptMaycBidsRequest, + ]) + ).to.be.revertedWith(ProtocolErrors.NOT_SAME_NTOKEN_OWNER); + }); +}); From 64dfb29bed0ad335d3f72876725e81f76480d19c Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Thu, 25 May 2023 09:46:27 +0800 Subject: [PATCH 2/7] chore: fix typo --- test/_blur_sell_integraion_marketplace.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/_blur_sell_integraion_marketplace.spec.ts b/test/_blur_sell_integraion_marketplace.spec.ts index 0b7c2728d..110bcfe6b 100644 --- a/test/_blur_sell_integraion_marketplace.spec.ts +++ b/test/_blur_sell_integraion_marketplace.spec.ts @@ -203,7 +203,7 @@ describe("BLUR Sell Integration Tests", () => { ).to.be.revertedWith(ProtocolErrors.INVALID_PAYMENT_TOKEN); }); - it("only pool admin can enable/disable acdept blur bids", async () => { + it("only pool admin can enable/disable accept blur bids", async () => { const { pool, users: [user1], From 9e1907096b2e4325c36606ad7e51ddd93ea36327 Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Fri, 26 May 2023 09:12:34 +0800 Subject: [PATCH 3/7] chore: check ape pair staking and gas optimization --- .../libraries/logic/PoolExtendedLogic.sol | 57 ++++++++++++++++--- .../libraries/logic/ValidationLogic.sol | 8 +-- contracts/protocol/pool/PoolMarketplace.sol | 5 +- .../_blur_sell_integraion_marketplace.spec.ts | 35 ++++++++++++ 4 files changed, 90 insertions(+), 15 deletions(-) diff --git a/contracts/protocol/libraries/logic/PoolExtendedLogic.sol b/contracts/protocol/libraries/logic/PoolExtendedLogic.sol index dfa6955d6..e1f592d81 100644 --- a/contracts/protocol/libraries/logic/PoolExtendedLogic.sol +++ b/contracts/protocol/libraries/logic/PoolExtendedLogic.sol @@ -22,9 +22,11 @@ import {ApeCoinStaking} from "../../../dependencies/yoga-labs/ApeCoinStaking.sol import {INTokenApeStaking} from "../../../interfaces/INTokenApeStaking.sol"; import {NTokenBAKC} from "../../tokenization/NTokenBAKC.sol"; import {XTokenType, IXTokenType} from "../../../interfaces/IXTokenType.sol"; +import {ReserveConfiguration} from "../configuration/ReserveConfiguration.sol"; library PoolExtendedLogic { using Math for uint256; + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; using UserConfiguration for DataTypes.UserConfigurationMap; using PercentageMath for uint256; using SafeCast for uint256; @@ -362,8 +364,9 @@ library PoolExtendedLogic { function executeInitiateAcceptBlurBidsRequest( DataTypes.PoolStorage storage ps, - IPoolAddressesProvider poolAddressProvider, - DataTypes.AcceptBlurBidsRequest[] calldata requests + DataTypes.AcceptBlurBidsRequest[] calldata requests, + address oracle, + address weth ) external { address keeper = ps._acceptBlurBidsKeeper; //check and update overall status @@ -381,7 +384,6 @@ library PoolExtendedLogic { } // validate user's health factor, if HF drops below 1 before keeper finalize the request, nToken can be liquidated. - address oracle = poolAddressProvider.getPriceOracle(); ValidationLogic.validateHealthFactor( ps._reserves, ps._reservesList, @@ -392,8 +394,11 @@ library PoolExtendedLogic { ); //validate and handle every single request - address weth = poolAddressProvider.getWETH(); uint256 requestFeeRate = ps._acceptBlurBidsRequestFeeRate; + uint256 wethLiquidationThreshold = _getWETHLiquidationThreashold( + ps, + weth + ); uint256 totalFee = 0; for (uint256 index = 0; index < requests.length; index++) { DataTypes.AcceptBlurBidsRequest calldata request = requests[index]; @@ -411,21 +416,38 @@ library PoolExtendedLogic { request, requestHash, weth, - oracle + oracle, + wethLiquidationThreshold ); //check if any ape coin position exist on the Ape { XTokenType tokenType = INToken(nTokenAddress).getXTokenType(); if (tokenType == XTokenType.NTokenBAYC) { + ApeCoinStaking apeCoinStaking = INTokenApeStaking( + nTokenAddress + ).getApeStaking(); _ensureApeCoinPositionNotExistedOn( - INTokenApeStaking(nTokenAddress).getApeStaking(), + apeCoinStaking, + 1, + request.tokenId + ); + _ensureApeIsNotPairedStaking( + apeCoinStaking, 1, request.tokenId ); } else if (tokenType == XTokenType.NTokenMAYC) { + ApeCoinStaking apeCoinStaking = INTokenApeStaking( + nTokenAddress + ).getApeStaking(); _ensureApeCoinPositionNotExistedOn( - INTokenApeStaking(nTokenAddress).getApeStaking(), + apeCoinStaking, + 2, + request.tokenId + ); + _ensureApeIsNotPairedStaking( + apeCoinStaking, 2, request.tokenId ); @@ -755,4 +777,25 @@ library PoolExtendedLogic { (uint256 stakedAmount, ) = apeStaking.nftPosition(poolId, tokenId); require(stakedAmount == 0, Errors.EXISTING_APE_STAKING); } + + function _ensureApeIsNotPairedStaking( + ApeCoinStaking apeStaking, + uint256 mainPoolId, + uint256 tokenId + ) internal view { + (, bool isPaired) = apeStaking.mainToBakc(mainPoolId, tokenId); + require(!isPaired, Errors.EXISTING_APE_STAKING); + } + + function _getWETHLiquidationThreashold( + DataTypes.PoolStorage storage ps, + address weth + ) internal view returns (uint256) { + DataTypes.ReserveConfigurationMap memory wethReserveConfiguration = ps + ._reserves[weth] + .configuration; + (, uint256 wethLiquidationThreshold, , , ) = wethReserveConfiguration + .getParams(); + return wethLiquidationThreshold; + } } diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 15fd94aeb..db210e02e 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -61,7 +61,8 @@ library ValidationLogic { DataTypes.AcceptBlurBidsRequest calldata request, bytes32 requestHash, address weth, - address oracle + address oracle, + uint256 wethLiquidationThreshold ) internal view { require( ps._acceptBlurBidsRequestStatus[requestHash] == @@ -83,11 +84,6 @@ library ValidationLogic { .configuration; (, uint256 nftLiquidationThreshold, , , ) = nftReserveConfiguration .getParams(); - DataTypes.ReserveConfigurationMap memory wethReserveConfiguration = ps - ._reserves[weth] - .configuration; - (, uint256 wethLiquidationThreshold, , , ) = wethReserveConfiguration - .getParams(); uint256 userReceivedCurrency = request.bidingPrice - request.marketPlaceFee; require( diff --git a/contracts/protocol/pool/PoolMarketplace.sol b/contracts/protocol/pool/PoolMarketplace.sol index 1781da900..ad2759ddd 100644 --- a/contracts/protocol/pool/PoolMarketplace.sol +++ b/contracts/protocol/pool/PoolMarketplace.sol @@ -205,8 +205,9 @@ contract PoolMarketplace is DataTypes.PoolStorage storage ps = poolStorage(); PoolExtendedLogic.executeInitiateAcceptBlurBidsRequest( ps, - ADDRESSES_PROVIDER, - requests + requests, + ADDRESSES_PROVIDER.getPriceOracle(), + ADDRESSES_PROVIDER.getWETH() ); } diff --git a/test/_blur_sell_integraion_marketplace.spec.ts b/test/_blur_sell_integraion_marketplace.spec.ts index 110bcfe6b..0a950f506 100644 --- a/test/_blur_sell_integraion_marketplace.spec.ts +++ b/test/_blur_sell_integraion_marketplace.spec.ts @@ -452,6 +452,41 @@ describe("BLUR Sell Integration Tests", () => { ).to.be.revertedWith(ProtocolErrors.EXISTING_APE_STAKING); }); + it("user can't initiate request if ape have paired staking position", async () => { + const { + pool, + users: [user1], + bayc, + bakc, + ape, + } = await loadFixture(fixture); + + await supplyAndValidate(bakc, "1", user1, true); + await mintAndValidate(ape, "20000", user1); + await waitForTx( + await ape.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT) + ); + + await waitForTx( + await pool.connect(user1.signer).borrowApeAndStake( + { + nftAsset: bayc.address, + borrowAsset: ape.address, + borrowAmount: 0, + cashAmount: parseEther("10000"), + }, + [], + [{mainTokenId: 0, bakcTokenId: 0, amount: parseEther("10000")}] + ) + ); + + await expect( + pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + ).to.be.revertedWith(ProtocolErrors.EXISTING_APE_STAKING); + }); + it("biding price * ls must > floor price * ls when initiate request", async () => { const { pool, From 300172c1c4cb44cd85f824e3a007cc51995d65ef Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Tue, 30 May 2023 15:19:16 +0800 Subject: [PATCH 4/7] chore: check owner and fix interest rate issue --- .../libraries/logic/PoolExtendedLogic.sol | 43 ++++++++++--------- .../libraries/logic/ValidationLogic.sol | 17 +++++--- .../_blur_sell_integraion_marketplace.spec.ts | 25 +++++++++++ 3 files changed, 58 insertions(+), 27 deletions(-) diff --git a/contracts/protocol/libraries/logic/PoolExtendedLogic.sol b/contracts/protocol/libraries/logic/PoolExtendedLogic.sol index e1f592d81..56d5a3617 100644 --- a/contracts/protocol/libraries/logic/PoolExtendedLogic.sol +++ b/contracts/protocol/libraries/logic/PoolExtendedLogic.sol @@ -141,6 +141,18 @@ library PoolExtendedLogic { require(remainingETH == 0, Errors.INVALID_ETH_VALUE); } + //transfer currency to keeper + if (totalBorrow > 0) { + DataTypes.TimeLockParams memory timeLockParams; + IPToken(ps._reserves[weth].xTokenAddress).transferUnderlyingTo( + address(this), + totalBorrow, + timeLockParams + ); + IWETH(weth).withdraw(totalBorrow); + } + Helpers.safeTransferETH(keeper, msg.value + totalBorrow); + //mint debt token if (totalBorrow > 0) { BorrowLogic.executeBorrow( @@ -161,18 +173,6 @@ library PoolExtendedLogic { }) ); } - - //transfer currency to keeper - if (totalBorrow > 0) { - DataTypes.TimeLockParams memory timeLockParams; - IPToken(ps._reserves[weth].xTokenAddress).transferUnderlyingTo( - address(this), - totalBorrow, - timeLockParams - ); - IWETH(weth).withdraw(totalBorrow); - } - Helpers.safeTransferETH(keeper, msg.value + totalBorrow); } function initiateBlurExchangeRequest( @@ -185,14 +185,15 @@ library PoolExtendedLogic { ) internal returns (uint256) { bytes32 requestHash = _calculateBlurExchangeRequestHash(request); uint256 requestFee = request.listingPrice.percentMul(requestFeeRate); - ValidationLogic.validateInitiateBlurExchangeRequest( - ps._reserves[request.collection], - request, - ps._blurExchangeRequestStatus[requestHash], - remainingETH, - requestFee, - oracle - ); + uint256 needCashETH = ValidationLogic + .validateInitiateBlurExchangeRequest( + ps._reserves[request.collection], + request, + ps._blurExchangeRequestStatus[requestHash], + remainingETH, + requestFee, + oracle + ); //mint nToken to release credit value DataTypes.ERC721SupplyParams[] @@ -225,7 +226,7 @@ library PoolExtendedLogic { request.tokenId ); - return request.listingPrice + requestFee - request.borrowAmount; + return needCashETH; } function executeFulfillBlurExchangeRequest( diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index db210e02e..9738b22a9 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -70,6 +70,11 @@ library ValidationLogic { Errors.INVALID_REQUEST_STATUS ); require(msg.sender == request.initiator, Errors.CALLER_NOT_INITIATOR); + require( + INToken(nTokenAddress).ownerOf(request.tokenId) == + request.initiator, + Errors.NOT_THE_OWNER + ); require(request.paymentToken == weth, Errors.INVALID_PAYMENT_TOKEN); uint256 floorPrice = IPriceOracleGetter(oracle).getAssetPrice( request.collection @@ -114,17 +119,16 @@ library ValidationLogic { uint256 remainingETH, uint256 requestFee, address oracle - ) internal view { + ) internal view returns (uint256) { require( requestStatus == DataTypes.BlurBuyWithCreditRequestStatus.Default, Errors.INVALID_REQUEST_STATUS ); require(msg.sender == request.initiator, Errors.CALLER_NOT_INITIATOR); - require( - remainingETH >= - request.listingPrice + requestFee - request.borrowAmount, - Errors.INVALID_ETH_VALUE - ); + uint256 needCashETH = request.listingPrice + + requestFee - + request.borrowAmount; + require(remainingETH >= needCashETH, Errors.INVALID_ETH_VALUE); require( request.paymentToken == address(0), Errors.INVALID_PAYMENT_TOKEN @@ -142,6 +146,7 @@ library ValidationLogic { request.listingPrice >= collateralPrice, Errors.INVALID_REQUEST_PRICE ); + return needCashETH; } /** diff --git a/test/_blur_sell_integraion_marketplace.spec.ts b/test/_blur_sell_integraion_marketplace.spec.ts index 0a950f506..4db2fbd14 100644 --- a/test/_blur_sell_integraion_marketplace.spec.ts +++ b/test/_blur_sell_integraion_marketplace.spec.ts @@ -761,4 +761,29 @@ describe("BLUR Sell Integration Tests", () => { ]) ).to.be.revertedWith(ProtocolErrors.NOT_SAME_NTOKEN_OWNER); }); + + it("initiate request failed when accept blur bids request disabled", async () => { + const { + pool, + weth, + bayc, + users: [, , user3], + } = await loadFixture(fixture); + + const InvalidAcceptBaycBidsRequest = { + initiator: user3.address, + paymentToken: weth.address, + bidingPrice: parseEther("110"), + marketPlaceFee: parseEther("1"), + collection: bayc.address, + tokenId: 0, + bidOrderHash: solidityKeccak256(["uint256"], [0]), + }; + + await expect( + pool + .connect(user3.signer) + .initiateAcceptBlurBidsRequest([InvalidAcceptBaycBidsRequest]) + ).to.be.revertedWith(ProtocolErrors.NOT_THE_OWNER); + }); }); From 533c1033aabf6c908e66dd25e157914fb9faed6e Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Tue, 30 May 2023 17:14:52 +0800 Subject: [PATCH 5/7] chore: keeper fulfill request with ETH --- contracts/interfaces/IPoolMarketplace.sol | 2 +- .../libraries/logic/PoolExtendedLogic.sol | 10 +++-- contracts/protocol/pool/PoolMarketplace.sol | 2 +- .../_blur_sell_integraion_marketplace.spec.ts | 41 ++++++++++++++++--- 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/contracts/interfaces/IPoolMarketplace.sol b/contracts/interfaces/IPoolMarketplace.sol index e5a958750..b3a6fe117 100644 --- a/contracts/interfaces/IPoolMarketplace.sol +++ b/contracts/interfaces/IPoolMarketplace.sol @@ -256,7 +256,7 @@ interface IPoolMarketplace { */ function fulfillAcceptBlurBidsRequest( DataTypes.AcceptBlurBidsRequest[] calldata requests - ) external; + ) external payable; /** * @notice Reject accept blur bids for underlying request if the blur selling transaction is failed. diff --git a/contracts/protocol/libraries/logic/PoolExtendedLogic.sol b/contracts/protocol/libraries/logic/PoolExtendedLogic.sol index 56d5a3617..2da993ae3 100644 --- a/contracts/protocol/libraries/logic/PoolExtendedLogic.sol +++ b/contracts/protocol/libraries/logic/PoolExtendedLogic.sol @@ -502,7 +502,7 @@ library PoolExtendedLogic { require(msg.sender == keeper, Errors.CALLER_NOT_KEEPER); uint256 requestLength = requests.length; - uint256 totalWETH = 0; + uint256 totalETH = 0; address currentOwner; for (uint256 index = 0; index < requestLength; index++) { DataTypes.AcceptBlurBidsRequest calldata request = requests[index]; @@ -533,7 +533,7 @@ library PoolExtendedLogic { } // calculate and accumulate weth - totalWETH += (request.bidingPrice - request.marketPlaceFee); + totalETH += (request.bidingPrice - request.marketPlaceFee); // update request status delete ps._blurExchangeRequestStatus[requestHash]; @@ -561,11 +561,13 @@ library PoolExtendedLogic { request.bidOrderHash ); } + require(msg.value == totalETH, Errors.INVALID_ETH_VALUE); //supply eth for current ntoken owner - if (totalWETH > 0) { + if (totalETH > 0) { address weth = poolAddressProvider.getWETH(); - supplyForUser(ps, weth, keeper, currentOwner, totalWETH); + IWETH(weth).deposit{value: msg.value}(); + supplyForUser(ps, weth, address(this), currentOwner, totalETH); } // update ongoing request amount diff --git a/contracts/protocol/pool/PoolMarketplace.sol b/contracts/protocol/pool/PoolMarketplace.sol index ad2759ddd..bfe24d07f 100644 --- a/contracts/protocol/pool/PoolMarketplace.sol +++ b/contracts/protocol/pool/PoolMarketplace.sol @@ -214,7 +214,7 @@ contract PoolMarketplace is /// @inheritdoc IPoolMarketplace function fulfillAcceptBlurBidsRequest( DataTypes.AcceptBlurBidsRequest[] calldata requests - ) external virtual override { + ) external payable override { DataTypes.PoolStorage storage ps = poolStorage(); PoolExtendedLogic.executeFulfillAcceptBlurBidsRequest( ps, diff --git a/test/_blur_sell_integraion_marketplace.spec.ts b/test/_blur_sell_integraion_marketplace.spec.ts index 4db2fbd14..0ddad6382 100644 --- a/test/_blur_sell_integraion_marketplace.spec.ts +++ b/test/_blur_sell_integraion_marketplace.spec.ts @@ -125,10 +125,12 @@ describe("BLUR Sell Integration Tests", () => { await waitForTx( await pool .connect(user2.signer) - .fulfillAcceptBlurBidsRequest([ - AcceptBaycBidsRequest, - AcceptMaycBidsRequest, - ]) + .fulfillAcceptBlurBidsRequest( + [AcceptBaycBidsRequest, AcceptMaycBidsRequest], + { + value: parseEther("168"), + } + ) ); almostEqual(await pWETH.balanceOf(user1.address), parseEther("168")); @@ -698,7 +700,9 @@ describe("BLUR Sell Integration Tests", () => { await waitForTx( await pool .connect(user2.signer) - .fulfillAcceptBlurBidsRequest([AcceptBaycBidsRequest]) + .fulfillAcceptBlurBidsRequest([AcceptBaycBidsRequest], { + value: parseEther("109"), + }) ); expect(await nBAYC.balanceOf(user1.address)).to.be.eq(0); @@ -786,4 +790,31 @@ describe("BLUR Sell Integration Tests", () => { .initiateAcceptBlurBidsRequest([InvalidAcceptBaycBidsRequest]) ).to.be.revertedWith(ProtocolErrors.NOT_THE_OWNER); }); + + it("fulfill requests failed if transaction value is wrong", async () => { + const { + pool, + users: [user1, user2], + } = await loadFixture(fixture); + + await waitForTx( + await pool + .connect(user1.signer) + .initiateAcceptBlurBidsRequest([ + AcceptBaycBidsRequest, + AcceptMaycBidsRequest, + ]) + ); + + await expect( + pool + .connect(user2.signer) + .fulfillAcceptBlurBidsRequest( + [AcceptBaycBidsRequest, AcceptMaycBidsRequest], + { + value: parseEther("100"), + } + ) + ).to.be.revertedWith(ProtocolErrors.INVALID_ETH_VALUE); + }); }); From 3a01c2b456af3e627c0ea484847067e5408865bc Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Wed, 31 May 2023 10:51:08 +0800 Subject: [PATCH 6/7] chore: fix updating wrong status --- contracts/protocol/libraries/logic/PoolExtendedLogic.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/protocol/libraries/logic/PoolExtendedLogic.sol b/contracts/protocol/libraries/logic/PoolExtendedLogic.sol index 2da993ae3..fde9d8da7 100644 --- a/contracts/protocol/libraries/logic/PoolExtendedLogic.sol +++ b/contracts/protocol/libraries/logic/PoolExtendedLogic.sol @@ -536,7 +536,7 @@ library PoolExtendedLogic { totalETH += (request.bidingPrice - request.marketPlaceFee); // update request status - delete ps._blurExchangeRequestStatus[requestHash]; + delete ps._acceptBlurBidsRequestStatus[requestHash]; //burn ntoken burnUserNToken( @@ -593,7 +593,7 @@ library PoolExtendedLogic { ); // update request status - delete ps._blurExchangeRequestStatus[requestHash]; + delete ps._acceptBlurBidsRequestStatus[requestHash]; //transfer underlying nft back to nToken DataTypes.ReserveData storage nftReserve = ps._reserves[ From fe2fe3ff8039815952c9e2f3f91e93a5b439a5f1 Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Wed, 31 May 2023 13:20:07 +0800 Subject: [PATCH 7/7] chore: forbid initiate request for uniswapV3 and stakeFish --- .../libraries/logic/ValidationLogic.sol | 9 +++++++- test/_blur_integration_marketplace.spec.ts | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 9738b22a9..8cee49cd8 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -125,6 +125,13 @@ library ValidationLogic { Errors.INVALID_REQUEST_STATUS ); require(msg.sender == request.initiator, Errors.CALLER_NOT_INITIATOR); + address nTokenAddress = nftReserve.xTokenAddress; + XTokenType tokenType = INToken(nTokenAddress).getXTokenType(); + require( + tokenType != XTokenType.NTokenUniswapV3 && + tokenType != XTokenType.NTokenStakefish, + Errors.XTOKEN_TYPE_NOT_ALLOWED + ); uint256 needCashETH = request.listingPrice + requestFee - request.borrowAmount; @@ -137,7 +144,7 @@ library ValidationLogic { request.collection ); uint256 collateralPrice = Helpers.getTraitBoostedTokenPrice( - nftReserve.xTokenAddress, + nTokenAddress, floorPrice, request.tokenId ); diff --git a/test/_blur_integration_marketplace.spec.ts b/test/_blur_integration_marketplace.spec.ts index 48d7feec1..ff525709d 100644 --- a/test/_blur_integration_marketplace.spec.ts +++ b/test/_blur_integration_marketplace.spec.ts @@ -740,4 +740,27 @@ describe("BLUR Buy Integration Tests", () => { }) ).to.be.revertedWith(ProtocolErrors.NOT_SAME_NTOKEN_OWNER); }); + + it("can't initiate request for uniswap V3", async () => { + const { + pool, + users: [user1], + nftPositionManager, + } = await loadFixture(fixture); + + const invalidRequest = { + initiator: user1.address, + paymentToken: zeroAddress(), + listingPrice: parseEther("100"), + borrowAmount: parseEther("20"), + collection: nftPositionManager.address, + tokenId: 0, + }; + + await expect( + pool.connect(user1.signer).initiateBlurExchangeRequest([invalidRequest], { + value: parseEther("80"), + }) + ).to.be.revertedWith(ProtocolErrors.XTOKEN_TYPE_NOT_ALLOWED); + }); });