diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 052c360a8..cc44e5ef3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,13 +31,12 @@ jobs: - name: Run lint run: | - yarn cache clean --all - YARN_CHECKSUM_BEHAVIOR=update yarn + touch .env yarn - yarn build - yarn doc - yarn lint - yarn size + make build + make doc + make lint + make size tests: name: Unit Tests @@ -65,11 +64,10 @@ jobs: - name: Run unit tests run: | - yarn cache clean --all - YARN_CHECKSUM_BEHAVIOR=update yarn + touch .env yarn - yarn build - yarn test + make build + make test coverage: name: Coverage Check @@ -95,11 +93,10 @@ jobs: with: node-version: ${{ matrix.node }} - run: | - yarn cache clean --all - YARN_CHECKSUM_BEHAVIOR=update yarn + touch .env yarn - yarn build - yarn coverage + make build + make coverage env: NODE_OPTIONS: --max_old_space_size=8192 - name: Publish coverage diff --git a/Makefile b/Makefile index ac78b0127..a33ff8c45 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,6 @@ doc: .PHONY: lint lint: - typos yarn lint .PHONY: coverage @@ -104,6 +103,18 @@ test-erc20-liquidation: test-erc20-borrow: make TEST_TARGET=_pool_core_erc20_borrow.spec.ts test +.PHONY: test-erc20-borrow-swap +test-erc20-borrow-swap: + make TEST_TARGET=_pool_core_erc20_borrow_swap.spec.ts test + +.PHONY: test-erc20-ptoken-swap +test-erc20-ptoken-swap: + make TEST_TARGET=_pool_core_erc20_ptoken_swap.spec.ts test + +.PHONY: test-erc20-debt-swap +test-erc20-debt-swap: + make TEST_TARGET=_pool_core_erc20_debt_swap.spec.ts test + .PHONY: test-erc20-supply test-erc20-supply: make TEST_TARGET=_pool_core_erc20_supply.spec.ts test @@ -172,6 +183,10 @@ test-moonbirds: test-marketplace-buy: make TEST_TARGET=_pool_marketplace_buy_with_credit.spec.ts test +.PHONY: test-marketplace-buy-any +test-marketplace-buy-any: + make TEST_TARGET=_pool_marketplace_buy_any_with_credit.spec.ts test + .PHONY: test-marketplace-accept-bid test-marketplace-accept-bid: make TEST_TARGET=_pool_marketplace_accept_bid_with_credit.spec.ts test diff --git a/contracts/account-abstraction/base-account-abstraction/core/BaseAccount.sol b/contracts/account-abstraction/base-account-abstraction/core/BaseAccount.sol index 41fa00c70..c4c7e7dae 100644 --- a/contracts/account-abstraction/base-account-abstraction/core/BaseAccount.sol +++ b/contracts/account-abstraction/base-account-abstraction/core/BaseAccount.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.10; +pragma solidity ^0.8.0; /* solhint-disable avoid-low-level-calls */ /* solhint-disable no-empty-blocks */ diff --git a/contracts/account-abstraction/base-account-abstraction/interfaces/IAccount.sol b/contracts/account-abstraction/base-account-abstraction/interfaces/IAccount.sol index 2ca5e949c..9e114ea1d 100644 --- a/contracts/account-abstraction/base-account-abstraction/interfaces/IAccount.sol +++ b/contracts/account-abstraction/base-account-abstraction/interfaces/IAccount.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.10; +pragma solidity ^0.8.0; import {UserOperation} from "../utils/UserOperation.sol"; diff --git a/contracts/account-abstraction/base-account-abstraction/interfaces/IAggregator.sol b/contracts/account-abstraction/base-account-abstraction/interfaces/IAggregator.sol index c0df8656e..7425525fb 100644 --- a/contracts/account-abstraction/base-account-abstraction/interfaces/IAggregator.sol +++ b/contracts/account-abstraction/base-account-abstraction/interfaces/IAggregator.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.10; +pragma solidity ^0.8.0; import {UserOperation} from "../utils/UserOperation.sol"; diff --git a/contracts/account-abstraction/base-account-abstraction/interfaces/IEntryPoint.sol b/contracts/account-abstraction/base-account-abstraction/interfaces/IEntryPoint.sol index ee3dca804..a5469060e 100644 --- a/contracts/account-abstraction/base-account-abstraction/interfaces/IEntryPoint.sol +++ b/contracts/account-abstraction/base-account-abstraction/interfaces/IEntryPoint.sol @@ -3,7 +3,7 @@ ** Only one instance required on each chain. **/ // SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.10; +pragma solidity ^0.8.0; /* solhint-disable avoid-low-level-calls */ /* solhint-disable no-inline-assembly */ diff --git a/contracts/account-abstraction/base-account-abstraction/interfaces/INonceManager.sol b/contracts/account-abstraction/base-account-abstraction/interfaces/INonceManager.sol index baf652dcc..aab9e3b12 100644 --- a/contracts/account-abstraction/base-account-abstraction/interfaces/INonceManager.sol +++ b/contracts/account-abstraction/base-account-abstraction/interfaces/INonceManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.10; +pragma solidity ^0.8.0; interface INonceManager { /** diff --git a/contracts/account-abstraction/base-account-abstraction/interfaces/IPaymaster.sol b/contracts/account-abstraction/base-account-abstraction/interfaces/IPaymaster.sol index bdfe1a03b..f6f687c9d 100644 --- a/contracts/account-abstraction/base-account-abstraction/interfaces/IPaymaster.sol +++ b/contracts/account-abstraction/base-account-abstraction/interfaces/IPaymaster.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.10; +pragma solidity ^0.8.0; import {UserOperation} from "../utils/UserOperation.sol"; diff --git a/contracts/account-abstraction/base-account-abstraction/interfaces/IStakeManager.sol b/contracts/account-abstraction/base-account-abstraction/interfaces/IStakeManager.sol index 2b8446726..711ffb6fe 100644 --- a/contracts/account-abstraction/base-account-abstraction/interfaces/IStakeManager.sol +++ b/contracts/account-abstraction/base-account-abstraction/interfaces/IStakeManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.10; +pragma solidity ^0.8.0; /** * manage deposits and stakes. diff --git a/contracts/account-abstraction/base-account-abstraction/utils/UserOperation.sol b/contracts/account-abstraction/base-account-abstraction/utils/UserOperation.sol index 4659f193a..2e7208d2a 100644 --- a/contracts/account-abstraction/base-account-abstraction/utils/UserOperation.sol +++ b/contracts/account-abstraction/base-account-abstraction/utils/UserOperation.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.10; +pragma solidity ^0.8.0; /** * User Operation struct diff --git a/contracts/dependencies/uniswapv3-core/libraries/BytesLib.sol b/contracts/dependencies/uniswapv3-core/libraries/BytesLib.sol new file mode 100644 index 000000000..fb7964cde --- /dev/null +++ b/contracts/dependencies/uniswapv3-core/libraries/BytesLib.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * @title Solidity Bytes Arrays Utils + * @author Gonçalo Sá + * + * @dev Bytes tightly packed arrays utility library for ethereum contracts written in Solidity. + * The library lets you concatenate, slice and type cast bytes arrays both in memory and storage. + */ +pragma solidity ^0.8.0; + +library BytesLib { + function slice( + bytes memory _bytes, + uint256 _start, + uint256 _length + ) internal pure returns (bytes memory) { + require(_length + 31 >= _length, 'slice_overflow'); + require(_start + _length >= _start, 'slice_overflow'); + require(_bytes.length >= _start + _length, 'slice_outOfBounds'); + + bytes memory tempBytes; + + assembly { + switch iszero(_length) + case 0 { + // Get a location of some free memory and store it in tempBytes as + // Solidity does for memory variables. + tempBytes := mload(0x40) + + // The first word of the slice result is potentially a partial + // word read from the original array. To read it, we calculate + // the length of that partial word and start copying that many + // bytes into the array. The first word we copy will start with + // data we don't care about, but the last `lengthmod` bytes will + // land at the beginning of the contents of the new array. When + // we're done copying, we overwrite the full first word with + // the actual length of the slice. + let lengthmod := and(_length, 31) + + // The multiplication in the next line is necessary + // because when slicing multiples of 32 bytes (lengthmod == 0) + // the following copy loop was copying the origin's length + // and then ending prematurely not copying everything it should. + let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod))) + let end := add(mc, _length) + + for { + // The multiplication in the next line has the same exact purpose + // as the one above. + let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start) + } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + mstore(mc, mload(cc)) + } + + mstore(tempBytes, _length) + + //update free-memory pointer + //allocating the array padded to 32 bytes like the compiler does now + mstore(0x40, and(add(mc, 31), not(31))) + } + //if we want a zero-length slice let's just return a zero-length array + default { + tempBytes := mload(0x40) + //zero out the 32 bytes slice we are about to return + //we need to do it because Solidity does not garbage collect + mstore(tempBytes, 0) + + mstore(0x40, add(tempBytes, 0x20)) + } + } + + return tempBytes; + } + + function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) { + require(_start + 20 >= _start, 'toAddress_overflow'); + require(_bytes.length >= _start + 20, 'toAddress_outOfBounds'); + address tempAddress; + + assembly { + tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000) + } + + return tempAddress; + } + + function toUint24(bytes memory _bytes, uint256 _start) internal pure returns (uint24) { + require(_start + 3 >= _start, 'toUint24_overflow'); + require(_bytes.length >= _start + 3, 'toUint24_outOfBounds'); + uint24 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x3), _start)) + } + + return tempUint; + } +} diff --git a/contracts/dependencies/uniswapv3-core/libraries/Path.sol b/contracts/dependencies/uniswapv3-core/libraries/Path.sol new file mode 100644 index 000000000..a397a7a6a --- /dev/null +++ b/contracts/dependencies/uniswapv3-core/libraries/Path.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {BytesLib} from './BytesLib.sol'; + +/// @title Functions for manipulating path data for multihop swaps +library Path { + using BytesLib for bytes; + + /// @dev The length of the bytes encoded address + uint256 private constant ADDR_SIZE = 20; + /// @dev The length of the bytes encoded fee + uint256 private constant FEE_SIZE = 3; + + /// @dev The offset of a single token address and pool fee + uint256 private constant NEXT_OFFSET = ADDR_SIZE + FEE_SIZE; + /// @dev The offset of an encoded pool key + uint256 private constant POP_OFFSET = NEXT_OFFSET + ADDR_SIZE; + /// @dev The minimum length of an encoding that contains 2 or more pools + uint256 private constant MULTIPLE_POOLS_MIN_LENGTH = POP_OFFSET + NEXT_OFFSET; + + /// @notice Returns true iff the path contains two or more pools + /// @param path The encoded swap path + /// @return True if path contains two or more pools, otherwise false + function hasMultiplePools(bytes memory path) internal pure returns (bool) { + return path.length >= MULTIPLE_POOLS_MIN_LENGTH; + } + + /// @notice Returns the number of pools in the path + /// @param path The encoded swap path + /// @return The number of pools in the path + function numPools(bytes memory path) internal pure returns (uint256) { + // Ignore the first token address. From then on every fee and token offset indicates a pool. + return ((path.length - ADDR_SIZE) / NEXT_OFFSET); + } + + /// @notice Decodes the first pool in path + /// @param path The bytes encoded swap path + /// @return tokenA The first token of the given pool + /// @return tokenB The second token of the given pool + /// @return fee The fee level of the pool + function decodeFirstPool(bytes memory path) + internal + pure + returns ( + address tokenA, + address tokenB, + uint24 fee + ) + { + tokenA = path.toAddress(0); + fee = path.toUint24(ADDR_SIZE); + tokenB = path.toAddress(NEXT_OFFSET); + } + + /// @notice Gets the segment corresponding to the first pool in the path + /// @param path The bytes encoded swap path + /// @return The segment containing all data necessary to target the first pool in the path + function getFirstPool(bytes memory path) internal pure returns (bytes memory) { + return path.slice(0, POP_OFFSET); + } + + /// @notice Skips a token + fee element from the buffer and returns the remainder + /// @param path The swap path + /// @return The remaining token + fee elements in the path + function skipToken(bytes memory path) internal pure returns (bytes memory) { + return path.slice(NEXT_OFFSET, path.length - NEXT_OFFSET); + } +} diff --git a/contracts/interfaces/IOneInch.sol b/contracts/interfaces/IOneInch.sol new file mode 100644 index 000000000..bba34be04 --- /dev/null +++ b/contracts/interfaces/IOneInch.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +interface IAggregationExecutor { + /// @notice propagates information about original msg.sender and executes arbitrary data + function execute(address msgSender) external payable; // 0x4b64e492 +} + +interface IOneInch { + struct SwapDescription { + address srcToken; + address dstToken; + address payable srcReceiver; + address payable dstReceiver; + uint256 amount; + uint256 minReturnAmount; + uint256 flags; + } + + function swap( + IAggregationExecutor executor, + SwapDescription calldata desc, + bytes calldata permit, + bytes calldata data + ) external payable returns (uint256 returnAmount, uint256 spentAmount); +} diff --git a/contracts/interfaces/IPToken.sol b/contracts/interfaces/IPToken.sol index 772fe5e4f..15b9c41d6 100644 --- a/contracts/interfaces/IPToken.sol +++ b/contracts/interfaces/IPToken.sol @@ -50,6 +50,27 @@ interface IPToken is DataTypes.TimeLockParams calldata timeLockParams ) external; + /** + * @notice Swap some tokens from another address, and then burn them + * @param from The address to swap the tokens from + * @param receiverOfUnderlying The address that will receive the underlying tokens after redemption + * @param index The index of the option that is being exercised + * @param timeLockParams The timelock parameters that determine when the tokens will be redeemed + * @param swapAdapter The address of the adapter contract that should be used for the swap + * @param swapPayload The payload data to pass to the swap adapter contract + * @param swapInfo The swap information that includes the details of the expected token swap + * @return The number of tokens that have been burned + */ + function swapAndBurnFrom( + address from, + address receiverOfUnderlying, + uint256 index, + DataTypes.TimeLockParams calldata timeLockParams, + DataTypes.SwapAdapter calldata swapAdapter, + bytes calldata swapPayload, + DataTypes.SwapInfo calldata swapInfo + ) external returns (uint256); + /** * @notice Mints xTokens to the reserve treasury * @param amount The amount of tokens getting minted @@ -74,6 +95,7 @@ interface IPToken is * @dev Used by the Pool to transfer assets in borrow(), withdraw() and flashLoan() * @param user The recipient of the underlying * @param amount The amount getting transferred + * @param timeLockParams The timelock parameters determined by timelock strategy **/ function transferUnderlyingTo( address user, @@ -82,14 +104,21 @@ interface IPToken is ) external; /** - * @notice Handles the underlying received by the xToken after the transfer has been completed. - * @dev The default implementation is empty as with standard ERC20 tokens, nothing needs to be done after the - * transfer is concluded. However in the future there may be xTokens that allow for example to stake the underlying - * to receive LM rewards. In that case, `handleRepayment()` would perform the staking of the underlying asset. - * @param user The user executing the repayment - * @param amount The amount getting repaid + * @notice Swap the underlying asset to `target`. + * @dev Used by the Pool to swap assets in borrow(), buyWithCredit() and acceptBidWithCredit() + * @param user The recipient of the swapped assets + * @param timeLockParams The timelock parameters determined by timelock strategy + * @param swapAdapter The swap adapter used for swapping. By default it'll be UniswapV3SwapAdapter + * @param swapPayload The swap payload + * @param swapInfo the swap info used for providing context information **/ - function handleRepayment(address user, uint256 amount) external; + function swapAndTransferUnderlyingTo( + address user, + DataTypes.TimeLockParams calldata timeLockParams, + DataTypes.SwapAdapter calldata swapAdapter, + bytes calldata swapPayload, + DataTypes.SwapInfo calldata swapInfo + ) external returns (uint256 amount); /** * @notice Allow passing a signed message to approve spending diff --git a/contracts/interfaces/IPoolCore.sol b/contracts/interfaces/IPoolCore.sol index 6f409fdd7..47a447587 100644 --- a/contracts/interfaces/IPoolCore.sol +++ b/contracts/interfaces/IPoolCore.sol @@ -27,6 +27,15 @@ interface IPoolCore { uint16 indexed referralCode ); + /** + * @dev Emitted on supplyERC721() + * @param reserve The address of the underlying asset of the reserve + * @param user The address initiating the supply + * @param onBehalfOf The beneficiary of the supply, receiving the xTokens + * @param tokenData The info of supplied tokens + * @param referralCode The referral code used + * @param fromNToken whether the supply call comes from NToken + **/ event SupplyERC721( address indexed reserve, address user, @@ -198,6 +207,38 @@ interface IPoolCore { uint256 indexed collateralTokenId ); + /** + * @notice Fired when a specified amount of pToken is swapped for another asset. + * @param srcReserve The address of the source reserve asset. + * @param dstReserve The address of the destination reserve asset. + * @param user The address of the user who initiated the swap. + * @param srcAmount The amount of pToken that was swapped. + * @param dstAmount The amount of the asset received after the swap was completed. + */ + event SwapPToken( + address indexed srcReserve, + address indexed dstReserve, + address indexed user, + uint256 srcAmount, + uint256 dstAmount + ); + + /** + * @notice Fired when a specified amount of debt from one asset is swapped for another. + * @param srcReserve The address of the asset that was swapped. + * @param dstReserve The address of the asset that was received in exchange. + * @param user The address of the user who initiated the swap. + * @param srcAmount The amount of `srcReserve` asset that was swapped. + * @param dstAmount The amount of `dstReserve` asset received after the swap was completed. + */ + event SwapDebt( + address indexed srcReserve, + address indexed dstReserve, + address indexed user, + uint256 srcAmount, + uint256 dstAmount + ); + /** * @dev Allows smart contracts to access the tokens within one transaction, as long as the tokens taken is returned. * @@ -364,6 +405,27 @@ interface IPoolCore { address onBehalfOf ) external; + /** + * @notice Borrow a specified amount of an asset from Aave V2 using any compatible adapter. + * @dev The user's account must already have a sufficient amount of collateral deposited in the protocol. + * @dev The `asset` must be an ERC20 token with allowance granted to this contract. + * @dev The borrow is executed using the `swapAdapterId` and `swapPayload` parameters to identify the specific adapter and its configuration. + * @param asset The address of the asset to be borrowed. + * @param amount The amount of the asset to be borrowed. + * @param referralCode An optional referral code to be used for rewarding the referrer, if any. + * @param onBehalfOf The address of the user to borrow on behalf of (if specified). + * @param swapAdapterId The ID of the adapter to be used for the borrow. + * @param swapPayload Additional data to be passed to the adapter to configure the borrow. + */ + function borrowAny( + address asset, + uint256 amount, + uint16 referralCode, + address onBehalfOf, + bytes32 swapAdapterId, + bytes calldata swapPayload + ) external; + /** * @notice Repays a borrowed `amount` on a specific reserve, burning the equivalent debt tokens owned * - E.g. User repays 100 USDC, burning 100 variable/stable debt tokens of the `onBehalfOf` address @@ -463,14 +525,64 @@ interface IPoolCore { bool receivePToken ) external payable; + /** + * @notice Function to liquidate a non-healthy position collateral-wise, with ERC721 Health Factor below 1 + * - The caller (liquidator) covers `liquidationAmount` amount of debt of the user getting liquidated, and receives + * a token of the `collateralAsset` plus a bonus to cover market risk + * @param collateralAsset The address of the underlying asset used as collateral, to receive as result of the liquidation + * @param user The address of the borrower getting liquidated + * @param collateralTokenId Then tokenId of the underlying asset used as collateral, to receive as result of the liquidation + * @param maxLiquidationAmount The max debt amount of borrowed `asset` the liquidator wants to cover + * @param receiveNToken True if the liquidators wants to receive the collateral xTokens, `false` if he wants + * to receive the underlying collateral asset directly + **/ function liquidateERC721( address collateralAsset, address user, uint256 collateralTokenId, - uint256 liquidationAmount, + uint256 maxLiquidationAmount, bool receiveNToken ) external payable; + /** + * @notice Swaps a specified amount of pToken for another asset using a specified adapter. + * @dev The user must have approved this contract to spend `srcAsset` tokens on their behalf. + * @dev The caller will receive `dstAsset` tokens after the swap is completed. + * @dev The swap is executed using the `swapAdapterId` and `swapPayload` parameters to identify the specific adapter and its configuration. + * @param srcAsset The address of the pToken to be swapped. + * @param srcAmount The amount of pToken to be swapped. + * @param dstAsset The address of the asset to be received after the swap is completed. + * @param to The address of the recipient of the `dstAsset` tokens. + * @param swapAdapterId The ID of the adapter to be used for the swap. + * @param swapPayload Additional data to be passed to the adapter to configure the swap. + */ + function swapPToken( + address srcAsset, + uint256 srcAmount, + address dstAsset, + address to, + bytes32 swapAdapterId, + bytes calldata swapPayload + ) external; + + /** + * @notice Swaps a specified amount of debt from one asset to another using a specified adapter. + * @dev The user must have approved this contract to spend `srcAsset` tokens on their behalf. + * @dev The swap is executed using the `swapAdapterId` and `swapPayload` parameters to identify the specific adapter and its configuration. + * @param srcAsset The address of the asset to be swapped from. + * @param srcAmount The amount of debt to be swapped. + * @param dstAsset The address of the asset to be swapped to (which represents the debt). + * @param swapAdapterId The ID of the adapter to be used for the swap. + * @param swapPayload Additional data to be passed to the adapter to configure the swap. + */ + function swapDebt( + address srcAsset, + uint256 srcAmount, + address dstAsset, + bytes32 swapAdapterId, + bytes calldata swapPayload + ) external; + /** * @notice Start the auction on user's specific NFT collateral * @param user The address of the user @@ -540,6 +652,11 @@ interface IPoolCore { address asset ) external view returns (DataTypes.ReserveData memory); + /** + * @notice Returns the corresponding xToken address for a given reserve asset. + * @param asset The address of the reserve asset to check. + * @return The address of the corresponding xToken for the specified reserve asset, or zero if not available. + */ function getReserveXToken(address asset) external view returns (address); /** @@ -615,6 +732,10 @@ interface IPoolCore { view returns (IPoolAddressesProvider); + /** + * @notice Returns the time-lock contract used by the protocol. + * @return A contract implementing the `ITimeLock` interface. + */ function TIME_LOCK() external view returns (ITimeLock); /** diff --git a/contracts/interfaces/IPoolMarketplace.sol b/contracts/interfaces/IPoolMarketplace.sol index d38af622c..e1c14801f 100644 --- a/contracts/interfaces/IPoolMarketplace.sol +++ b/contracts/interfaces/IPoolMarketplace.sol @@ -29,13 +29,30 @@ interface IPoolMarketplace { * @param marketplaceId The marketplace identifier * @param payload The encoded parameters to be passed to marketplace contract (selector eliminated) * @param credit The credit that user would like to use for this purchase - * @param referralCode The referral code used */ function buyWithCredit( + bytes32 marketplaceId, + bytes calldata payload, + DataTypes.Credit calldata credit + ) external payable; + + /** + * @notice Purchase an asset through a specified marketplace using the user's available credit. + * @dev The credit must have been previously approved for use by the user, and the marketplace must support the `marketplaceId`. + * @dev The payment for the asset is deducted from the user's available credit, and any remaining credit is returned to the user. + * @dev The purchase is executed using the `swapAdapterId` and `swapPayload` parameters to identify the specific adapter and its configuration. + * @param marketplaceId The ID of the marketplace to purchase the asset from. + * @param payload Additional data to be passed to the marketplace to configure the purchase. + * @param credit A `DataTypes.Credit` struct specifying the credit to be used for the purchase. + * @param swapAdapterId The ID of the adapter to be used for the swap. + * @param swapPayload Additional data to be passed to the adapter to configure the swap. + */ + function buyAnyWithCredit( bytes32 marketplaceId, bytes calldata payload, DataTypes.Credit calldata credit, - uint16 referralCode + bytes32 swapAdapterId, + bytes calldata swapPayload ) external payable; /** @@ -45,13 +62,30 @@ interface IPoolMarketplace { * @param marketplaceIds The marketplace identifiers * @param payloads The encoded parameters to be passed to marketplace contract (selector eliminated) * @param credits The credits that user would like to use for this purchase - * @param referralCode The referral code used */ function batchBuyWithCredit( + bytes32[] calldata marketplaceIds, + bytes[] calldata payloads, + DataTypes.Credit[] calldata credits + ) external payable; + + /** + * @notice Purchase multiple assets through multiple marketplaces using the user's available credit. + * @dev The credit must have been previously approved for use by the user, and the marketplaces must support the specified `marketplaceIds`. + * @dev The payment for each asset is deducted from the user's available credit, and any remaining credit is returned to the user. + * @dev The purchase for each asset is executed using the corresponding `swapAdapters` and `swapPayloads` in the same order as the `marketplaceIds`. + * @param marketplaceIds An array of marketplace IDs to purchase assets from. + * @param payloads An array of additional data to be passed to each marketplace to configure the respective purchases. + * @param credits An array of `DataTypes.Credit` structs specifying the credits to be used for each purchase. + * @param swapAdapters An array of `DataTypes.SwapAdapter` structs specifying the adapter to be used for each purchase. + * @param swapPayloads An array of additional data to be passed to each adapter to configure the respective swaps. + */ + function batchBuyAnyWithCredit( bytes32[] calldata marketplaceIds, bytes[] calldata payloads, DataTypes.Credit[] calldata credits, - uint16 referralCode + DataTypes.SwapAdapter[] calldata swapAdapters, + bytes[] calldata swapPayloads ) external payable; /** @@ -63,14 +97,12 @@ interface IPoolMarketplace { * @param payload The encoded parameters to be passed to marketplace contract (selector eliminated) * @param credit The credit that user would like to use for this purchase * @param onBehalfOf Address of the user who will sell the NFT - * @param referralCode The referral code used */ function acceptBidWithCredit( bytes32 marketplaceId, bytes calldata payload, DataTypes.Credit calldata credit, - address onBehalfOf, - uint16 referralCode + address onBehalfOf ) external; /** @@ -82,13 +114,35 @@ interface IPoolMarketplace { * @param payloads The encoded parameters to be passed to marketplace contract (selector eliminated) * @param credits The credits that the makers have approved to use for this purchase * @param onBehalfOf Address of the user who will sell the NFTs - * @param referralCode The referral code used */ function batchAcceptBidWithCredit( bytes32[] calldata marketplaceIds, bytes[] calldata payloads, DataTypes.Credit[] calldata credits, - address onBehalfOf, - uint16 referralCode + address onBehalfOf + ) external; + + /** + * @notice Accepts an OpenSea bid + * @param marketplaceId The unique identifier of the marketplace where the bid is made + * @param payload The bytes data of the bid payload + * @param onBehalfOf The address of the account (the bidder, or the seller with the OpenSea account authorized) that will execute the transaction + */ + function acceptOpenseaBid( + bytes32 marketplaceId, + bytes calldata payload, + address onBehalfOf + ) external; + + /** + * @notice Batch accepts OpenSea bids + * @param marketplaceIds The unique identifiers of the marketplace where the bid is made + * @param payloads The bytes data of the bid payloads + * @param onBehalfOf The address of the account (the bidder, or the seller with the OpenSea account authorized) that will execute the transaction + */ + function batchAcceptOpenseaBid( + bytes32[] calldata marketplaceIds, + bytes[] calldata payloads, + address onBehalfOf ) external; } diff --git a/contracts/interfaces/IPoolParameters.sol b/contracts/interfaces/IPoolParameters.sol index aa8691ee3..4e3679a9a 100644 --- a/contracts/interfaces/IPoolParameters.sol +++ b/contracts/interfaces/IPoolParameters.sol @@ -31,6 +31,16 @@ interface IPoolParameters { **/ event ClaimApeForYieldIncentiveUpdated(uint256 oldValue, uint256 newValue); + /** + * @dev Emitted when the swap adapter got updated + **/ + event SwapAdapterUpdated( + bytes32 indexed swapAdapterId, + address adapter, + address router, + bool paused + ); + /** * @notice Initializes a reserve, activating it, assigning an xToken and debt tokens and an * interest rate strategy @@ -152,6 +162,15 @@ interface IPoolParameters { address user ) external view returns (DataTypes.ApeCompoundStrategy memory); + function setSwapAdapter( + bytes32 swapAdapterId, + DataTypes.SwapAdapter calldata adapter + ) external; + + function getSwapAdapter( + bytes32 swapAdapterId + ) external view returns (DataTypes.SwapAdapter memory); + /** * @notice Set the auction recovery health factor * @param value The new auction health factor diff --git a/contracts/interfaces/ISwapAdapter.sol b/contracts/interfaces/ISwapAdapter.sol new file mode 100644 index 000000000..1c1c862a6 --- /dev/null +++ b/contracts/interfaces/ISwapAdapter.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import "../protocol/libraries/types/DataTypes.sol"; + +interface ISwapAdapter { + function getSwapInfo( + bytes memory payload, + bool exactInput + ) external view returns (DataTypes.SwapInfo memory); + + function swap( + address exchange, + bytes memory payload, + bool exactInput + ) external returns (uint256); +} diff --git a/contracts/misc/marketplaces/BlurAdapter.sol b/contracts/misc/marketplaces/BlurAdapter.sol index 031f9be5a..06cdfc55f 100644 --- a/contracts/misc/marketplaces/BlurAdapter.sol +++ b/contracts/misc/marketplaces/BlurAdapter.sol @@ -32,16 +32,11 @@ contract BlurAdapter is IMarketplace { (Input, Input) ); orderInfo.maker = sell.order.trader; - orderInfo.taker = buy.order.trader; require( sell.order.matchingPolicy == POLICY_ALLOWED, // must be StandardSaleForFixedPrice matching policy Errors.INVALID_MARKETPLACE_ORDER ); - require( - orderInfo.taker == ADDRESSES_PROVIDER.getPool(), - Errors.INVALID_ORDER_TAKER - ); OfferItem[] memory offer = new OfferItem[](1); offer[0] = OfferItem( @@ -64,9 +59,9 @@ contract BlurAdapter is IMarketplace { itemType, token, 0, - sell.order.price, - sell.order.price, - payable(buy.order.trader) + buy.order.price, + buy.order.price, + payable(sell.order.trader) ); orderInfo.id = abi.encodePacked(sell.r, sell.s, sell.v); orderInfo.consideration = consideration; diff --git a/contracts/misc/marketplaces/LooksRareAdapter.sol b/contracts/misc/marketplaces/LooksRareAdapter.sol index 72761f6ba..8a402dfbc 100644 --- a/contracts/misc/marketplaces/LooksRareAdapter.sol +++ b/contracts/misc/marketplaces/LooksRareAdapter.sol @@ -32,12 +32,7 @@ contract LooksRareAdapter is IMarketplace { OrderTypes.MakerOrder memory makerAsk ) = abi.decode(params, (OrderTypes.TakerOrder, OrderTypes.MakerOrder)); orderInfo.maker = makerAsk.signer; - orderInfo.taker = takerBid.taker; - require( - orderInfo.taker == ADDRESSES_PROVIDER.getPool(), - Errors.INVALID_ORDER_TAKER - ); require( makerAsk.strategy == STRATEGY_ALLOWED, // must be StandardSaleForFixedPrice strategy Errors.INVALID_MARKETPLACE_ORDER @@ -66,16 +61,16 @@ contract LooksRareAdapter is IMarketplace { itemType, token, 0, - makerAsk.price, // TODO: take minPercentageToAsk into account - makerAsk.price, - payable(takerBid.taker) + takerBid.price, // TODO: take minPercentageToAsk into account + takerBid.price, + payable(makerAsk.signer) ); orderInfo.id = abi.encodePacked(makerAsk.r, makerAsk.s, makerAsk.v); orderInfo.consideration = consideration; } function getBidOrderInfo( - bytes memory /*params*/ + bytes memory ) external pure override returns (DataTypes.OrderInfo memory) { revert(Errors.CALL_MARKETPLACE_FAILED); } diff --git a/contracts/misc/marketplaces/SeaportAdapter.sol b/contracts/misc/marketplaces/SeaportAdapter.sol index 7e08bf3fa..08ed56a70 100644 --- a/contracts/misc/marketplaces/SeaportAdapter.sol +++ b/contracts/misc/marketplaces/SeaportAdapter.sol @@ -24,47 +24,42 @@ contract SeaportAdapter is IMarketplace { function getAskOrderInfo( bytes memory params - ) external view override returns (DataTypes.OrderInfo memory orderInfo) { - ( - AdvancedOrder memory advancedOrder, - CriteriaResolver[] memory resolvers, - , - address recipient - ) = abi.decode( - params, - (AdvancedOrder, CriteriaResolver[], bytes32, address) - ); + ) external pure override returns (DataTypes.OrderInfo memory orderInfo) { + AdvancedOrder[] memory advancedOrders = abi.decode( + params, + (AdvancedOrder[]) + ); // support advanced order in the future require( // NOT criteria based and must be basic order - resolvers.length == 0 && isBasicOrder(advancedOrder), + advancedOrders.length == 2 && + isBasicOrder(advancedOrders[0]) && + isBasicOrder(advancedOrders[1]), Errors.INVALID_MARKETPLACE_ORDER ); - // the person who listed NFT to sell - orderInfo.maker = advancedOrder.parameters.offerer; - require( - recipient == ADDRESSES_PROVIDER.getPool(), - Errors.INVALID_ORDER_TAKER - ); + // the person who creates listing to sell NFT + orderInfo.maker = advancedOrders[0].parameters.offerer; + // the person who takes listing to buy NFT + orderInfo.taker = advancedOrders[1].parameters.offerer; - orderInfo.id = advancedOrder.signature; // NFT, items will be checked inside MarketplaceLogic - orderInfo.offer = advancedOrder.parameters.offer; + orderInfo.offer = advancedOrders[0].parameters.offer; require(orderInfo.offer.length > 0, Errors.INVALID_MARKETPLACE_ORDER); // ERC20, items will be checked inside MarketplaceLogic - orderInfo.consideration = advancedOrder.parameters.consideration; + orderInfo.consideration = advancedOrders[0].parameters.consideration; require( orderInfo.consideration.length > 0, Errors.INVALID_MARKETPLACE_ORDER ); + orderInfo.isSeaport = true; } function getBidOrderInfo( bytes memory params ) external pure override returns (DataTypes.OrderInfo memory orderInfo) { - (AdvancedOrder[] memory advancedOrders, , ) = abi.decode( + AdvancedOrder[] memory advancedOrders = abi.decode( params, - (AdvancedOrder[], CriteriaResolver[], Fulfillment[]) + (AdvancedOrder[]) ); // support advanced order in the future require( @@ -92,6 +87,7 @@ contract SeaportAdapter is IMarketplace { orderInfo.consideration.length > 0, Errors.INVALID_MARKETPLACE_ORDER ); + orderInfo.isSeaport = true; } function matchAskWithTakerBid( @@ -99,7 +95,7 @@ contract SeaportAdapter is IMarketplace { bytes calldata params, uint256 value ) external payable override returns (bytes memory) { - bytes4 selector = SeaportInterface.fulfillAdvancedOrder.selector; + bytes4 selector = SeaportInterface.matchAdvancedOrders.selector; bytes memory data = abi.encodePacked(selector, params); return Address.functionCallWithValue( diff --git a/contracts/misc/marketplaces/X2Y2Adapter.sol b/contracts/misc/marketplaces/X2Y2Adapter.sol index 3963cd69b..ec4a1b0ab 100644 --- a/contracts/misc/marketplaces/X2Y2Adapter.sol +++ b/contracts/misc/marketplaces/X2Y2Adapter.sol @@ -29,7 +29,7 @@ contract X2Y2Adapter is IMarketplace { function getAskOrderInfo( bytes memory params - ) external pure override returns (DataTypes.OrderInfo memory orderInfo) { + ) external view override returns (DataTypes.OrderInfo memory orderInfo) { IX2Y2.RunInput memory runInput = abi.decode(params, (IX2Y2.RunInput)); require(runInput.details.length == 1, Errors.INVALID_MARKETPLACE_ORDER); @@ -51,6 +51,8 @@ contract X2Y2Adapter is IMarketplace { require(nfts.length == 1, Errors.INVALID_MARKETPLACE_ORDER); + orderInfo.maker = order.user; + OfferItem[] memory offer = new OfferItem[](1); offer[0] = OfferItem( ItemType.ERC721, diff --git a/contracts/misc/swap-adapters/UniswapV3SwapAdapter.sol b/contracts/misc/swap-adapters/UniswapV3SwapAdapter.sol new file mode 100644 index 000000000..63866a992 --- /dev/null +++ b/contracts/misc/swap-adapters/UniswapV3SwapAdapter.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {ISwapAdapter} from "../../interfaces/ISwapAdapter.sol"; +import {ISwapRouter} from "../../dependencies/uniswapv3-periphery/interfaces/ISwapRouter.sol"; +import {BytesLib} from "../../dependencies/uniswapv3-core/libraries/BytesLib.sol"; +import {DataTypes} from "../../protocol/libraries/types/DataTypes.sol"; +import {Errors} from "../../protocol/libraries/helpers/Errors.sol"; +import {Address} from "../../dependencies/openzeppelin/contracts/Address.sol"; + +contract UniswapV3SwapAdapter is ISwapAdapter { + using BytesLib for bytes; + + uint256 private constant ADDR_SIZE = 20; + uint256 private constant FEE_SIZE = 3; + + function getSwapInfo( + bytes memory payload, + bool exactInput + ) external pure returns (DataTypes.SwapInfo memory) { + if (exactInput) { + return _getExactInputParams(payload); + } else { + return _getExactOutputParams(payload); + } + } + + function swap( + address router, + bytes memory payload, + bool exactInput + ) external returns (uint256) { + bytes4 selector = exactInput + ? ISwapRouter.exactInput.selector + : ISwapRouter.exactOutput.selector; + bytes memory data = abi.encodePacked(selector, payload); + bytes memory returnData = Address.functionCall( + router, + data, + Errors.CALL_SWAP_FAILED + ); + return abi.decode(returnData, (uint256)); + } + + function _getExactInputParams( + bytes memory payload + ) internal pure returns (DataTypes.SwapInfo memory swapInfo) { + ISwapRouter.ExactInputParams memory params = abi.decode( + bytes(payload), + (ISwapRouter.ExactInputParams) + ); + + address srcToken = params.path.toAddress(0); + address dstToken = params.path.toAddress( + params.path.length - ADDR_SIZE + ); + + swapInfo.srcToken = srcToken; + swapInfo.dstToken = dstToken; + swapInfo.maxAmountIn = params.amountIn; + swapInfo.minAmountOut = params.amountOutMinimum; + swapInfo.srcReceiver = address(0); + swapInfo.dstReceiver = params.recipient; + swapInfo.exactInput = true; + } + + function _getExactOutputParams( + bytes memory payload + ) internal pure returns (DataTypes.SwapInfo memory swapInfo) { + ISwapRouter.ExactOutputParams memory params = abi.decode( + bytes(payload), + (ISwapRouter.ExactOutputParams) + ); + + address dstToken = params.path.toAddress(0); + address srcToken = params.path.toAddress( + params.path.length - ADDR_SIZE + ); + + swapInfo.srcToken = srcToken; + swapInfo.dstToken = dstToken; + swapInfo.maxAmountIn = params.amountInMaximum; + swapInfo.minAmountOut = params.amountOut; + swapInfo.srcReceiver = address(0); + swapInfo.dstReceiver = params.recipient; + swapInfo.exactInput = false; + } +} diff --git a/contracts/mocks/upgradeability/MockPoolCore.sol b/contracts/mocks/upgradeability/MockPoolCore.sol index 70b16430b..c94444a16 100644 --- a/contracts/mocks/upgradeability/MockPoolCore.sol +++ b/contracts/mocks/upgradeability/MockPoolCore.sol @@ -27,7 +27,6 @@ import {ParaReentrancyGuard} from "../../protocol/libraries/paraspace-upgradeabi import {IAuctionableERC721} from "../../interfaces/IAuctionableERC721.sol"; import {IReserveAuctionStrategy} from "../../interfaces/IReserveAuctionStrategy.sol"; import {ITimeLock} from "../../interfaces/ITimeLock.sol"; - import {IPoolCore} from "../../interfaces/IPoolCore.sol"; import {PoolCore} from "../../protocol/pool/PoolCore.sol"; diff --git a/contracts/protocol/libraries/helpers/Errors.sol b/contracts/protocol/libraries/helpers/Errors.sol index c10efa7b1..78d657f1d 100644 --- a/contracts/protocol/libraries/helpers/Errors.sol +++ b/contracts/protocol/libraries/helpers/Errors.sol @@ -133,6 +133,9 @@ library Errors { string public constant CALLER_NOT_OPERATOR = "138"; // The caller of the function is not operator string public constant INVALID_FEE_VALUE = "139"; // invalid fee rate value string public constant TOKEN_NOT_ALLOW_RESCUE = "140"; // token is not allow rescue + string public constant CALL_SWAP_FAILED = "141"; //call swap failed. + string public constant INVALID_SWAP_PAYLOAD = "142"; //invalid swap payload. + string public constant SWAP_ADAPTER_PAUSED = "143"; //swap adapter paused. string public constant INVALID_PARAMETER = "170"; //invalid parameter } diff --git a/contracts/protocol/libraries/helpers/Helpers.sol b/contracts/protocol/libraries/helpers/Helpers.sol index 98643d73a..f1f93c40e 100644 --- a/contracts/protocol/libraries/helpers/Helpers.sol +++ b/contracts/protocol/libraries/helpers/Helpers.sol @@ -5,6 +5,7 @@ import {IERC20} from "../../../dependencies/openzeppelin/contracts/IERC20.sol"; import {DataTypes} from "../types/DataTypes.sol"; import {WadRayMath} from "../../libraries/math/WadRayMath.sol"; import {IAtomicCollateralizableERC721} from "../../../interfaces/IAtomicCollateralizableERC721.sol"; +import {SafeERC20} from "../../../dependencies/openzeppelin/contracts/SafeERC20.sol"; import {UserConfiguration} from "../configuration/UserConfiguration.sol"; /** @@ -13,6 +14,7 @@ import {UserConfiguration} from "../configuration/UserConfiguration.sol"; */ library Helpers { using WadRayMath for uint256; + using SafeERC20 for IERC20; using UserConfiguration for DataTypes.UserConfigurationMap; // See `IPool` for descriptions @@ -44,6 +46,13 @@ library Helpers { return assetPrice.wadMul(multiplier); } + function checkMaxAllowance(address token, address operator) internal { + uint256 allowance = IERC20(token).allowance(address(this), operator); + if (allowance == 0) { + IERC20(token).safeApprove(operator, type(uint256).max); + } + } + /** * @dev transfer ETH to an address, revert if it fails. * @param to recipient of the transfer diff --git a/contracts/protocol/libraries/logic/BorrowLogic.sol b/contracts/protocol/libraries/logic/BorrowLogic.sol index 1759bb7fd..1cab6f991 100644 --- a/contracts/protocol/libraries/logic/BorrowLogic.sol +++ b/contracts/protocol/libraries/logic/BorrowLogic.sol @@ -14,6 +14,7 @@ import {DataTypes} from "../types/DataTypes.sol"; import {ValidationLogic} from "./ValidationLogic.sol"; import {ReserveLogic} from "./ReserveLogic.sol"; import {GenericLogic} from "./GenericLogic.sol"; +import {ISwapAdapter} from "../../../interfaces/ISwapAdapter.sol"; /** * @title BorrowLogic library @@ -112,11 +113,34 @@ library BorrowLogic { ); timeLockParams.actionType = DataTypes.TimeLockActionType.BORROW; - IPToken(reserveCache.xTokenAddress).transferUnderlyingTo( - params.user, - params.amount, - timeLockParams - ); + if (params.swapAdapter.router == address(0)) { + IPToken(reserveCache.xTokenAddress).transferUnderlyingTo( + params.user, + params.amount, + timeLockParams + ); + } else { + DataTypes.SwapInfo memory swapInfo = ISwapAdapter( + params.swapAdapter.adapter + ).getSwapInfo(params.swapPayload, true); + ValidationLogic.validateSwap( + swapInfo, + DataTypes.ValidateSwapParams({ + swapAdapter: params.swapAdapter, + amount: params.amount, + srcToken: params.asset, + dstToken: address(0), + dstReceiver: reserveCache.xTokenAddress + }) + ); + IPToken(reserveCache.xTokenAddress).swapAndTransferUnderlyingTo( + params.user, + timeLockParams, + params.swapAdapter, + params.swapPayload, + swapInfo + ); + } } emit Borrow( @@ -209,10 +233,6 @@ library BorrowLogic { reserveCache.xTokenAddress, paybackAmount ); - IPToken(reserveCache.xTokenAddress).handleRepayment( - params.payer, - paybackAmount - ); } emit Repay( diff --git a/contracts/protocol/libraries/logic/LiquidationLogic.sol b/contracts/protocol/libraries/logic/LiquidationLogic.sol index 777c9e9a3..b1dbc291b 100644 --- a/contracts/protocol/libraries/logic/LiquidationLogic.sol +++ b/contracts/protocol/libraries/logic/LiquidationLogic.sol @@ -538,9 +538,6 @@ library LiquidationLogic { ExecuteLiquidateLocalVars memory vars ) internal { _depositETH(params, vars); - // Handle payment - IPToken(vars.liquidationAssetReserveCache.xTokenAddress) - .handleRepayment(params.liquidator, vars.actualLiquidationAmount); // Burn borrower's debt token vars .liquidationAssetReserveCache diff --git a/contracts/protocol/libraries/logic/MarketplaceLogic.sol b/contracts/protocol/libraries/logic/MarketplaceLogic.sol index e8ce12464..71521e7ca 100644 --- a/contracts/protocol/libraries/logic/MarketplaceLogic.sol +++ b/contracts/protocol/libraries/logic/MarketplaceLogic.sol @@ -22,9 +22,12 @@ import {ItemType} from "../../../dependencies/seaport/contracts/lib/Consideratio import {AdvancedOrder} from "../../../dependencies/seaport/contracts/lib/ConsiderationStructs.sol"; import {IWETH} from "../../../misc/interfaces/IWETH.sol"; import {UserConfiguration} from "../configuration/UserConfiguration.sol"; -import {ReserveConfiguration} from "../configuration/ReserveConfiguration.sol"; import {IMarketplace} from "../../../interfaces/IMarketplace.sol"; import {Address} from "../../../dependencies/openzeppelin/contracts/Address.sol"; +import {ISwapAdapter} from "../../../interfaces/ISwapAdapter.sol"; +import {Helpers} from "../../../protocol/libraries/helpers/Helpers.sol"; +import {MathUtils} from "../math/MathUtils.sol"; +import {WadRayMath} from "../../libraries/math/WadRayMath.sol"; /** * @title Marketplace library @@ -33,11 +36,11 @@ import {Address} from "../../../dependencies/openzeppelin/contracts/Address.sol" */ library MarketplaceLogic { using UserConfiguration for DataTypes.UserConfigurationMap; - using ReserveConfiguration for DataTypes.ReserveConfigurationMap; using ReserveLogic for DataTypes.ReserveData; using SafeERC20 for IERC20; using Math for uint256; using PercentageMath for uint256; + using WadRayMath for uint256; event ReserveUsedAsCollateralEnabled( address indexed reserve, @@ -51,12 +54,6 @@ library MarketplaceLogic { uint16 indexed referralCode ); - /** - * @dev Default percentage of listing price to be supplied on behalf of the seller in a marketplace exchange. - * Expressed in bps, a value of 0.95e4 results in 95.00% - */ - uint256 internal constant DEFAULT_SUPPLY_RATIO = 0.95e4; - event BuyWithCredit( bytes32 indexed marketplaceId, DataTypes.OrderInfo orderInfo, @@ -70,19 +67,20 @@ library MarketplaceLogic { ); struct MarketplaceLocalVars { - bool isETH; - address xTokenAddress; - uint256 price; + bool isListingTokenETH; + bool isListingTokenPToken; + bool isCollectionListed; + address listingToken; + address listingXTokenAddress; address creditToken; address creditXTokenAddress; + address collectionToken; + address collectionXTokenAddress; + uint256 listingTokenNextLiquidityIndex; uint256 creditAmount; + uint256 borrowAmount; uint256 supplyAmount; - address weth; - uint256 ethLeft; - bytes32 marketplaceId; - bytes payload; - DataTypes.Marketplace marketplace; - DataTypes.OrderInfo orderInfo; + uint256 price; } function executeBuyWithCredit( @@ -90,40 +88,28 @@ library MarketplaceLogic { bytes32 marketplaceId, bytes calldata payload, DataTypes.Credit calldata credit, - IPoolAddressesProvider poolAddressProvider, - uint16 referralCode + DataTypes.SwapAdapter calldata swapAdapter, + bytes calldata swapPayload, + IPoolAddressesProvider poolAddressProvider ) external { - MarketplaceLocalVars memory vars; - - vars.weth = poolAddressProvider.getWETH(); - DataTypes.Marketplace memory marketplace = poolAddressProvider - .getMarketplace(marketplaceId); - DataTypes.OrderInfo memory orderInfo = IMarketplace(marketplace.adapter) - .getAskOrderInfo(payload); - orderInfo.taker = msg.sender; - vars.ethLeft = msg.value; - - _depositETH(vars, orderInfo); - - vars.ethLeft -= _buyWithCredit( + DataTypes.ExecuteMarketplaceParams memory params = _initParams( ps, - DataTypes.ExecuteMarketplaceParams({ - marketplaceId: marketplaceId, - payload: payload, - credit: credit, - ethLeft: vars.ethLeft, - marketplace: marketplace, - orderInfo: orderInfo, - weth: vars.weth, - referralCode: referralCode, - reservesCount: ps._reservesCount, - oracle: poolAddressProvider.getPriceOracle(), - priceOracleSentinel: poolAddressProvider - .getPriceOracleSentinel() - }) + poolAddressProvider + ); + params.ethLeft = msg.value; + _updateBuyParams( + params, + poolAddressProvider, + marketplaceId, + payload, + credit, + swapAdapter, + swapPayload ); - _refundETH(vars.ethLeft); + _depositETH(params); + _buyWithCredit(ps, params); + _refundETH(params.ethLeft); } /** @@ -136,19 +122,23 @@ library MarketplaceLogic { function _buyWithCredit( DataTypes.PoolStorage storage ps, DataTypes.ExecuteMarketplaceParams memory params - ) internal returns (uint256) { + ) internal { ValidationLogic.validateBuyWithCredit(params); - MarketplaceLocalVars memory vars = _cache(ps, params); - - _flashSupplyFor(ps, params, vars, params.orderInfo.maker); - _flashLoanTo(params, vars, address(this)); - - (uint256 priceEth, uint256 downpaymentEth) = _delegateToPool( + MarketplaceLocalVars memory vars = _cache( + ps, params, - vars + params.orderInfo.taker ); + bool delegate = !params.orderInfo.isSeaport || vars.isListingTokenETH; + (address recipient, uint256 priceEth) = delegate + ? (address(this), _delegateToPool(params, vars)) + : (params.orderInfo.taker, 0); + + _flashSupplyFor(ps, vars, params.orderInfo.maker); + _flashLoanTo(ps, params, vars, recipient); + // delegateCall to avoid extra token transfer Address.functionDelegateCall( params.marketplace.adapter, @@ -160,7 +150,7 @@ library MarketplaceLogic { ) ); - _handleFlashSupplyRepayment(vars, params.orderInfo.maker); + _handleFlashSupplyRepayment(vars, params); _handleFlashLoanRepayment(ps, params, vars, params.orderInfo.taker); emit BuyWithCredit( @@ -168,8 +158,6 @@ library MarketplaceLogic { params.orderInfo, params.credit ); - - return downpaymentEth; } function executeBatchBuyWithCredit( @@ -177,30 +165,34 @@ library MarketplaceLogic { bytes32[] calldata marketplaceIds, bytes[] calldata payloads, DataTypes.Credit[] calldata credits, - IPoolAddressesProvider poolAddressProvider, - uint16 referralCode + DataTypes.SwapAdapter[] calldata swapAdapters, + bytes[] calldata swapPayloads, + IPoolAddressesProvider poolAddressProvider ) external { - MarketplaceLocalVars memory vars; - - vars.weth = poolAddressProvider.getWETH(); require( marketplaceIds.length == payloads.length && - payloads.length == credits.length, + swapAdapters.length == payloads.length && + swapPayloads.length == payloads.length && + credits.length == payloads.length, Errors.INCONSISTENT_PARAMS_LENGTH ); - vars.ethLeft = msg.value; - uint256 reservesCount = ps._reservesCount; - for (uint256 i = 0; i < marketplaceIds.length; i++) { - vars.marketplaceId = marketplaceIds[i]; - vars.payload = payloads[i]; - DataTypes.Credit memory credit = credits[i]; - DataTypes.Marketplace memory marketplace = poolAddressProvider - .getMarketplace(vars.marketplaceId); - DataTypes.OrderInfo memory orderInfo = IMarketplace( - marketplace.adapter - ).getAskOrderInfo(vars.payload); - orderInfo.taker = msg.sender; + DataTypes.ExecuteMarketplaceParams memory params = _initParams( + ps, + poolAddressProvider + ); + params.ethLeft = msg.value; + + for (uint256 i = 0; i < marketplaceIds.length; i++) { + _updateBuyParams( + params, + poolAddressProvider, + marketplaceIds[i], + payloads[i], + credits[i], + swapAdapters[i], + swapPayloads[i] + ); // Once we encounter a listing using WETH, then we convert all our ethLeft to WETH // this also means that the parameters order is very important @@ -216,28 +208,11 @@ library MarketplaceLogic { // batchBuyWithCredit([ETH, ETH, ETH]) => ok // batchBuyWithCredit([ETH, ETH, WETH]) => ok // - _depositETH(vars, orderInfo); - - vars.ethLeft -= _buyWithCredit( - ps, - DataTypes.ExecuteMarketplaceParams({ - marketplaceId: vars.marketplaceId, - payload: vars.payload, - credit: credit, - ethLeft: vars.ethLeft, - marketplace: marketplace, - orderInfo: orderInfo, - weth: vars.weth, - referralCode: referralCode, - reservesCount: reservesCount, - oracle: poolAddressProvider.getPriceOracle(), - priceOracleSentinel: poolAddressProvider - .getPriceOracleSentinel() - }) - ); + _depositETH(params); + _buyWithCredit(ps, params); } - _refundETH(vars.ethLeft); + _refundETH(params.ethLeft); } function executeAcceptBidWithCredit( @@ -246,35 +221,22 @@ library MarketplaceLogic { bytes calldata payload, DataTypes.Credit calldata credit, address onBehalfOf, - IPoolAddressesProvider poolAddressProvider, - uint16 referralCode + IPoolAddressesProvider poolAddressProvider ) external { - MarketplaceLocalVars memory vars; - - vars.weth = poolAddressProvider.getWETH(); - vars.marketplace = poolAddressProvider.getMarketplace(marketplaceId); - vars.orderInfo = IMarketplace(vars.marketplace.adapter).getBidOrderInfo( - payload - ); - require(vars.orderInfo.taker == onBehalfOf, Errors.INVALID_ORDER_TAKER); - - _acceptBidWithCredit( + DataTypes.ExecuteMarketplaceParams memory params = _initParams( ps, - DataTypes.ExecuteMarketplaceParams({ - marketplaceId: marketplaceId, - payload: payload, - credit: credit, - ethLeft: 0, - marketplace: vars.marketplace, - orderInfo: vars.orderInfo, - weth: vars.weth, - referralCode: referralCode, - reservesCount: ps._reservesCount, - oracle: poolAddressProvider.getPriceOracle(), - priceOracleSentinel: poolAddressProvider - .getPriceOracleSentinel() - }) + poolAddressProvider ); + _updateAcceptBidParams( + params, + poolAddressProvider, + marketplaceId, + payload, + credit, + onBehalfOf + ); + + _acceptBidWithCredit(ps, params); } function executeBatchAcceptBidWithCredit( @@ -283,50 +245,95 @@ library MarketplaceLogic { bytes[] calldata payloads, DataTypes.Credit[] calldata credits, address onBehalfOf, - IPoolAddressesProvider poolAddressProvider, - uint16 referralCode + IPoolAddressesProvider poolAddressProvider ) external { - MarketplaceLocalVars memory vars; - - vars.weth = poolAddressProvider.getWETH(); require( marketplaceIds.length == payloads.length && payloads.length == credits.length, Errors.INCONSISTENT_PARAMS_LENGTH ); - uint256 reservesCount = ps._reservesCount; + DataTypes.ExecuteMarketplaceParams memory params = _initParams( + ps, + poolAddressProvider + ); for (uint256 i = 0; i < marketplaceIds.length; i++) { - vars.marketplaceId = marketplaceIds[i]; - vars.payload = payloads[i]; - DataTypes.Credit memory credit = credits[i]; - - vars.marketplace = poolAddressProvider.getMarketplace( - vars.marketplaceId - ); - vars.orderInfo = IMarketplace(vars.marketplace.adapter) - .getBidOrderInfo(vars.payload); - require( - vars.orderInfo.taker == onBehalfOf, - Errors.INVALID_ORDER_TAKER + _updateAcceptBidParams( + params, + poolAddressProvider, + marketplaceIds[i], + payloads[i], + credits[i], + onBehalfOf ); - _acceptBidWithCredit( - ps, - DataTypes.ExecuteMarketplaceParams({ - marketplaceId: vars.marketplaceId, - payload: vars.payload, - credit: credit, - ethLeft: 0, - marketplace: vars.marketplace, - orderInfo: vars.orderInfo, - weth: vars.weth, - referralCode: referralCode, - reservesCount: reservesCount, - oracle: poolAddressProvider.getPriceOracle(), - priceOracleSentinel: poolAddressProvider - .getPriceOracleSentinel() - }) + _acceptBidWithCredit(ps, params); + } + } + + function executeAcceptOpenseaBid( + DataTypes.PoolStorage storage ps, + bytes32 marketplaceId, + bytes calldata payload, + address onBehalfOf, + IPoolAddressesProvider poolAddressProvider + ) external { + DataTypes.ExecuteMarketplaceParams memory params = _initParams( + ps, + poolAddressProvider + ); + _updateAcceptBidParams( + params, + poolAddressProvider, + marketplaceId, + payload, + DataTypes.Credit( + address(0), + 0, + bytes(""), + 0, + bytes32(""), + bytes32("") + ), + onBehalfOf + ); + + _acceptOpenseaBid(ps, params); + } + + function executeBatchAcceptOpenseaBid( + DataTypes.PoolStorage storage ps, + bytes32[] calldata marketplaceIds, + bytes[] calldata payloads, + address onBehalfOf, + IPoolAddressesProvider poolAddressProvider + ) external { + require( + marketplaceIds.length == payloads.length, + Errors.INCONSISTENT_PARAMS_LENGTH + ); + DataTypes.ExecuteMarketplaceParams memory params = _initParams( + ps, + poolAddressProvider + ); + + for (uint256 i = 0; i < marketplaceIds.length; i++) { + _updateAcceptBidParams( + params, + poolAddressProvider, + marketplaceIds[i], + payloads[i], + DataTypes.Credit( + address(0), + 0, + bytes(""), + 0, + bytes32(""), + bytes32("") + ), + onBehalfOf ); + + _acceptOpenseaBid(ps, params); } } @@ -344,10 +351,14 @@ library MarketplaceLogic { ) internal { ValidationLogic.validateAcceptBidWithCredit(params); - MarketplaceLocalVars memory vars = _cache(ps, params); + MarketplaceLocalVars memory vars = _cache( + ps, + params, + params.orderInfo.maker + ); - _flashSupplyFor(ps, params, vars, params.orderInfo.taker); - _flashLoanTo(params, vars, params.orderInfo.maker); + _flashSupplyFor(ps, vars, params.orderInfo.taker); + _flashLoanTo(ps, params, vars, params.orderInfo.maker); // delegateCall to avoid extra token transfer Address.functionDelegateCall( @@ -359,7 +370,7 @@ library MarketplaceLogic { ) ); - _handleFlashSupplyRepayment(vars, params.orderInfo.taker); + _handleFlashSupplyRepayment(vars, params); _handleFlashLoanRepayment(ps, params, vars, params.orderInfo.maker); emit AcceptBidWithCredit( @@ -369,34 +380,59 @@ library MarketplaceLogic { ); } - /** - * @notice Transfer payNow portion from taker to this contract. This is only useful - * in buyWithCredit. - * @dev - * @param params The additional parameters needed to execute the buyWithCredit/acceptBidWithCredit function - * @param vars The marketplace local vars for caching storage values for future reads - */ + function _acceptOpenseaBid( + DataTypes.PoolStorage storage ps, + DataTypes.ExecuteMarketplaceParams memory params + ) internal { + ValidationLogic.validateAcceptOpenseaBid(params); + + MarketplaceLocalVars memory vars = _cache( + ps, + params, + params.orderInfo.maker + ); + + _flashSupplyFor(ps, vars, params.orderInfo.taker); + _withdrawERC721For(ps, params, vars, params.orderInfo.taker); + + // delegateCall to avoid extra token transfer + Address.functionDelegateCall( + params.marketplace.adapter, + abi.encodeWithSelector( + IMarketplace.matchBidWithTakerAsk.selector, + params.marketplace.marketplace, + params.payload + ) + ); + + _handleFlashSupplyRepayment(vars, params); + + emit AcceptBidWithCredit( + params.marketplaceId, + params.orderInfo, + params.credit + ); + } + function _delegateToPool( DataTypes.ExecuteMarketplaceParams memory params, MarketplaceLocalVars memory vars - ) internal returns (uint256, uint256) { - uint256 price = vars.price; - uint256 downpayment = price - vars.creditAmount; - if (!vars.isETH) { - IERC20(vars.creditToken).safeTransferFrom( + ) internal returns (uint256 priceEth) { + uint256 downpayment = vars.price - vars.creditAmount; + if (!vars.isListingTokenETH) { + IERC20(params.orderInfo.consideration[0].token).safeTransferFrom( params.orderInfo.taker, address(this), downpayment ); - _checkAllowance(vars.creditToken, params.marketplace.operator); - // convert to (priceEth, downpaymentEth) - price = 0; - downpayment = 0; + Helpers.checkMaxAllowance( + params.orderInfo.consideration[0].token, + params.marketplace.operator + ); } else { - require(params.ethLeft >= downpayment, Errors.PAYNOW_NOT_ENOUGH); + params.ethLeft -= downpayment; + priceEth = vars.price; } - - return (price, downpayment); } /** @@ -406,9 +442,10 @@ library MarketplaceLogic { * @param ps The pool storage pointer * @param params The additional parameters needed to execute the buyWithCredit/acceptBidWithCredit function * @param vars The marketplace local vars for caching storage values for future reads - * @param to The receiver of borrowed tokens + * @param to The origin receiver of borrowed tokens */ function _flashLoanTo( + DataTypes.PoolStorage storage ps, DataTypes.ExecuteMarketplaceParams memory params, MarketplaceLocalVars memory vars, address to @@ -418,89 +455,166 @@ library MarketplaceLogic { } DataTypes.TimeLockParams memory timeLockParams; - IPToken(vars.creditXTokenAddress).transferUnderlyingTo( - to, - vars.creditAmount, - timeLockParams - ); + address transit = vars.isListingTokenPToken ? address(this) : to; + + if (vars.listingToken == vars.creditToken) { + vars.borrowAmount = vars.creditAmount; + IPToken(vars.creditXTokenAddress).transferUnderlyingTo( + transit, + vars.creditAmount, + timeLockParams + ); + } else { + DataTypes.SwapInfo memory swapInfo = ISwapAdapter( + params.swapAdapter.adapter + ).getSwapInfo(params.swapPayload, false); + ValidationLogic.validateSwap( + swapInfo, + DataTypes.ValidateSwapParams({ + swapAdapter: params.swapAdapter, + amount: vars.creditAmount, + srcToken: vars.creditToken, + dstToken: vars.listingToken, + dstReceiver: vars.creditXTokenAddress + }) + ); + vars.borrowAmount = IPToken(vars.creditXTokenAddress) + .swapAndTransferUnderlyingTo( + transit, + timeLockParams, + params.swapAdapter, + params.swapPayload, + swapInfo + ); + } - if (vars.isETH) { + if (vars.isListingTokenETH && transit == address(this)) { // No re-entrancy because it sent to our contract address IWETH(params.weth).withdraw(vars.creditAmount); + } else if (vars.isListingTokenPToken) { + SupplyLogic.executeSupply( + ps._reserves, + ps._usersConfig[to], + DataTypes.ExecuteSupplyParams({ + asset: vars.listingToken, + amount: vars.creditAmount, + onBehalfOf: to, + payer: transit, + referralCode: 0 + }) + ); } } /** - * @notice Flash mint 90% of listingPrice as pToken so that seller's NFT can be traded in advance. + * @notice Flash mint the supplyAmount of listingPrice as pToken so that seller's NFT can be traded in advance. * Repayment needs to be done after the marketplace exchange by transferring funds to xTokenAddress * @dev * @param ps The pool storage pointer - * @param params The additional parameters needed to execute the buyWithCredit/acceptBidWithCredit function * @param vars The marketplace local vars for caching storage values for future reads * @param seller The NFT seller */ function _flashSupplyFor( DataTypes.PoolStorage storage ps, - DataTypes.ExecuteMarketplaceParams memory params, MarketplaceLocalVars memory vars, address seller ) internal { - if (vars.isETH) { - return; // impossible to supply ETH on behalf of the + if (vars.supplyAmount == 0) { + return; } - DataTypes.ReserveData storage reserve = ps._reserves[vars.creditToken]; + DataTypes.ReserveData storage reserve = ps._reserves[vars.listingToken]; DataTypes.UserConfigurationMap storage sellerConfig = ps._usersConfig[ seller ]; DataTypes.ReserveCache memory reserveCache = reserve.cache(); uint16 reserveId = reserve.id; // cache to reduce one storage read - reserve.updateState(reserveCache); - - uint256 supplyAmount = Math.min( - IERC20(vars.creditToken).allowance(seller, address(this)), - vars.price.percentMul(DEFAULT_SUPPLY_RATIO) - ); - if (supplyAmount == 0) { - return; + bool willUpdateRateLater = (vars.isListingTokenPToken || + vars.listingToken == vars.creditToken) && vars.creditAmount != 0; + if (!willUpdateRateLater) { + reserve.updateState(reserveCache); + reserve.updateInterestRates( + reserveCache, + vars.listingToken, + vars.isListingTokenPToken ? 0 : vars.supplyAmount, + 0 + ); + vars.listingTokenNextLiquidityIndex = reserveCache + .nextLiquidityIndex; + } else { + uint256 cumulatedLiquidityInterest = MathUtils + .calculateLinearInterest( + reserveCache.currLiquidityRate, + reserveCache.reserveLastUpdateTimestamp + ); + vars.listingTokenNextLiquidityIndex = cumulatedLiquidityInterest + .rayMul(reserveCache.currLiquidityIndex); } - ValidationLogic.validateSupply( - reserveCache, - supplyAmount, - DataTypes.AssetType.ERC20 - ); - - reserve.updateInterestRates( - reserveCache, - vars.creditToken, - supplyAmount, - 0 - ); - bool isFirstSupply = IPToken(reserveCache.xTokenAddress).mint( msg.sender, seller, - supplyAmount, - reserveCache.nextLiquidityIndex + vars.supplyAmount, + vars.listingTokenNextLiquidityIndex ); if (isFirstSupply || !sellerConfig.isUsingAsCollateral(reserveId)) { sellerConfig.setUsingAsCollateral(reserveId, true); - emit ReserveUsedAsCollateralEnabled(vars.creditToken, seller); + emit ReserveUsedAsCollateralEnabled(vars.listingToken, seller); } + } - emit Supply( - vars.creditToken, - msg.sender, - seller, - supplyAmount, - params.referralCode - ); + function _withdrawERC721For( + DataTypes.PoolStorage storage ps, + DataTypes.ExecuteMarketplaceParams memory params, + MarketplaceLocalVars memory vars, + address seller + ) internal { + if (vars.collectionXTokenAddress == address(0)) { + return; + } + + uint256 size = params.orderInfo.offer.length; + uint256[] memory tokenIds = new uint256[](size); + uint256 amountToWithdraw; + for (uint256 i = 0; i < size; i++) { + OfferItem memory item = params.orderInfo.offer[i]; + uint256 tokenId = item.identifierOrCriteria; + require( + item.itemType == ItemType.ERC721, + Errors.INVALID_ASSET_TYPE + ); + require( + item.token == params.orderInfo.offer[0].token, + Errors.INVALID_MARKETPLACE_ORDER + ); - // set supplyAmount for future repayment - vars.supplyAmount = supplyAmount; + if ( + IERC721(vars.collectionXTokenAddress).ownerOf(tokenId) == seller + ) { + tokenIds[amountToWithdraw++] = tokenId; + } + } + + if (amountToWithdraw > 0) { + assembly { + mstore(tokenIds, amountToWithdraw) + } + SupplyLogic.executeWithdrawERC721( + ps._reserves, + ps._reservesList, + ps._usersConfig[seller], + DataTypes.ExecuteWithdrawERC721Params({ + asset: vars.collectionToken, + tokenIds: tokenIds, + to: seller, + reservesCount: params.reservesCount, + oracle: params.oracle, + timeLock: false + }) + ); + } } /** @@ -518,58 +632,7 @@ library MarketplaceLogic { MarketplaceLocalVars memory vars, address buyer ) internal { - for (uint256 i = 0; i < params.orderInfo.offer.length; i++) { - OfferItem memory item = params.orderInfo.offer[i]; - require( - item.itemType == ItemType.ERC721, - Errors.INVALID_ASSET_TYPE - ); - - // underlyingAsset - address token = item.token; - uint256 tokenId = item.identifierOrCriteria; - // NToken - vars.xTokenAddress = ps._reserves[token].xTokenAddress; - - // item.token == NToken - if (vars.xTokenAddress == address(0)) { - address underlyingAsset = INToken(token) - .UNDERLYING_ASSET_ADDRESS(); - bool isNToken = ps._reserves[underlyingAsset].xTokenAddress == - token; - require(isNToken, Errors.ASSET_NOT_LISTED); - vars.xTokenAddress = token; - token = underlyingAsset; - } - - require( - INToken(vars.xTokenAddress).getXTokenType() != - XTokenType.NTokenUniswapV3, - Errors.XTOKEN_TYPE_NOT_ALLOWED - ); - - // item.token == underlyingAsset but supplied after listing/offering - // so NToken is transferred instead - if (INToken(vars.xTokenAddress).ownerOf(tokenId) == address(this)) { - _transferAndCollateralize(ps, vars, buyer, token, tokenId); - // item.token == underlyingAsset and underlyingAsset stays in wallet - } else { - DataTypes.ERC721SupplyParams[] - memory tokenData = new DataTypes.ERC721SupplyParams[](1); - tokenData[0] = DataTypes.ERC721SupplyParams(tokenId, true); - SupplyLogic.executeSupplyERC721( - ps._reserves, - ps._usersConfig[buyer], - DataTypes.ExecuteSupplyERC721Params({ - asset: token, - tokenData: tokenData, - onBehalfOf: buyer, - payer: address(this), - referralCode: params.referralCode - }) - ); - } - } + _transferOrCollateralize(ps, params, vars, buyer); if (vars.creditAmount == 0) { return; @@ -583,12 +646,18 @@ library MarketplaceLogic { asset: vars.creditToken, user: buyer, onBehalfOf: buyer, - amount: vars.creditAmount, - referralCode: params.referralCode, + amount: vars.borrowAmount, + referralCode: 0, releaseUnderlying: false, reservesCount: params.reservesCount, oracle: params.oracle, - priceOracleSentinel: params.priceOracleSentinel + priceOracleSentinel: params.priceOracleSentinel, + swapAdapter: DataTypes.SwapAdapter( + address(0), + address(0), + false + ), + swapPayload: bytes("") }) ); } @@ -597,43 +666,147 @@ library MarketplaceLogic { * @notice "Repay" minted pToken by transferring funds from the seller to xTokenAddress * @dev * @param vars The marketplace local vars for caching storage values for future reads - * @param seller The NFT seller + * @param params The additional parameters needed to execute the buyWithCredit function */ function _handleFlashSupplyRepayment( MarketplaceLocalVars memory vars, - address seller + DataTypes.ExecuteMarketplaceParams memory params ) internal { - if (vars.isETH || vars.supplyAmount == 0) { + if (vars.supplyAmount == 0) { return; } - IERC20(vars.creditToken).safeTransferFrom( - seller, - vars.creditXTokenAddress, - vars.supplyAmount - ); - } + if (vars.isListingTokenPToken) { + DataTypes.TimeLockParams memory timeLockParams; + IPToken(vars.listingXTokenAddress).burn( + address(this), + vars.listingXTokenAddress, + vars.supplyAmount, + vars.listingTokenNextLiquidityIndex, + timeLockParams + ); + } else { + if (vars.isListingTokenETH) { + IWETH(params.weth).deposit{value: vars.supplyAmount}(); + } - function _checkAllowance(address token, address operator) internal { - uint256 allowance = IERC20(token).allowance(address(this), operator); - if (allowance == 0) { - IERC20(token).safeApprove(operator, type(uint256).max); + IERC20(vars.listingToken).safeTransfer( + vars.listingXTokenAddress, + vars.supplyAmount + ); } } function _cache( DataTypes.PoolStorage storage ps, - DataTypes.ExecuteMarketplaceParams memory params + DataTypes.ExecuteMarketplaceParams memory params, + address buyer ) internal view returns (MarketplaceLocalVars memory vars) { - vars.isETH = params.credit.token == address(0); - vars.creditToken = vars.isETH ? params.weth : params.credit.token; + vars.creditToken = params.credit.token; vars.creditAmount = params.credit.amount; - vars.price = _validateAndGetPrice(params, vars); - DataTypes.ReserveData storage reserve = ps._reserves[vars.creditToken]; - vars.creditXTokenAddress = reserve.xTokenAddress; + vars.creditXTokenAddress = ps._reserves[vars.creditToken].xTokenAddress; + + vars.isListingTokenETH = + params.orderInfo.consideration[0].token == address(0); + + if (vars.isListingTokenETH) { + vars.listingToken = params.weth; + vars.listingXTokenAddress = ps + ._reserves[vars.listingToken] + .xTokenAddress; + } else { + vars.listingToken = params.orderInfo.consideration[0].token; + vars.listingXTokenAddress = ps + ._reserves[vars.listingToken] + .xTokenAddress; + if (vars.listingXTokenAddress == address(0)) { + try + IPToken(vars.listingToken).UNDERLYING_ASSET_ADDRESS() + returns (address underlyingAsset) { + vars.isListingTokenPToken = + ps._reserves[underlyingAsset].xTokenAddress == + vars.listingToken; + if (vars.isListingTokenPToken) { + vars.listingXTokenAddress = vars.listingToken; + vars.listingToken = underlyingAsset; + } + } catch {} + } + } + + (vars.price, vars.supplyAmount) = _validateConsideration( + params, + vars, + buyer + ); + + _validateOffer(ps, params, vars); + } + + function _initParams( + DataTypes.PoolStorage storage ps, + IPoolAddressesProvider poolAddressProvider + ) internal view returns (DataTypes.ExecuteMarketplaceParams memory params) { + params.weth = poolAddressProvider.getWETH(); + params.reservesCount = ps._reservesCount; + params.oracle = poolAddressProvider.getPriceOracle(); + params.priceOracleSentinel = poolAddressProvider + .getPriceOracleSentinel(); + } + + function _updateBuyParams( + DataTypes.ExecuteMarketplaceParams memory params, + IPoolAddressesProvider poolAddressProvider, + bytes32 marketplaceId, + bytes memory payload, + DataTypes.Credit memory credit, + DataTypes.SwapAdapter memory swapAdapter, + bytes memory swapPayload + ) internal view { + params.marketplaceId = marketplaceId; + params.marketplace = poolAddressProvider.getMarketplace(marketplaceId); + params.payload = payload; + params.credit = credit; + params.swapAdapter = swapAdapter; + params.swapPayload = swapPayload; + + params.orderInfo = IMarketplace(params.marketplace.adapter) + .getAskOrderInfo(payload); + if (params.orderInfo.isSeaport) { + require( + msg.sender == params.orderInfo.taker, + Errors.INVALID_ORDER_TAKER + ); + } else { + // in LooksRare, X2Y2 we dont match orders between buyer and seller + // the protocol just works like an agent so taker cannot be read + // from orders + params.orderInfo.taker = msg.sender; + } + require( + params.orderInfo.maker != params.orderInfo.taker, + Errors.MAKER_SAME_AS_TAKER + ); + } + + function _updateAcceptBidParams( + DataTypes.ExecuteMarketplaceParams memory params, + IPoolAddressesProvider poolAddressProvider, + bytes32 marketplaceId, + bytes memory payload, + DataTypes.Credit memory credit, + address onBehalfOf + ) internal view { + params.marketplaceId = marketplaceId; + params.marketplace = poolAddressProvider.getMarketplace(marketplaceId); + params.payload = payload; + params.credit = credit; + + params.orderInfo = IMarketplace(params.marketplace.adapter) + .getBidOrderInfo(payload); require( - vars.creditXTokenAddress != address(0), - Errors.ASSET_NOT_LISTED + params.orderInfo.taker == onBehalfOf, + Errors.INVALID_ORDER_TAKER ); } @@ -644,69 +817,219 @@ library MarketplaceLogic { } function _depositETH( - MarketplaceLocalVars memory vars, - DataTypes.OrderInfo memory orderInfo + DataTypes.ExecuteMarketplaceParams memory params ) internal { if ( - vars.ethLeft == 0 || - orderInfo.consideration[0].itemType == ItemType.NATIVE + params.ethLeft == 0 || + params.orderInfo.consideration[0].itemType == ItemType.NATIVE ) { return; } - IWETH(vars.weth).deposit{value: vars.ethLeft}(); - IERC20(vars.weth).safeTransferFrom( + IWETH(params.weth).deposit{value: params.ethLeft}(); + IERC20(params.weth).safeTransferFrom( address(this), msg.sender, - vars.ethLeft + params.ethLeft ); - vars.ethLeft = 0; + + params.ethLeft = 0; } - function _transferAndCollateralize( + function _transferOrCollateralize( DataTypes.PoolStorage storage ps, + DataTypes.ExecuteMarketplaceParams memory params, MarketplaceLocalVars memory vars, - address buyer, - address token, - uint256 tokenId + address buyer ) internal { - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = tokenId; - - IERC721(vars.xTokenAddress).safeTransferFrom( - address(this), - buyer, - tokenId - ); - SupplyExtendedLogic.executeCollateralizeERC721( - ps._reserves, - ps._usersConfig[buyer], - token, - tokenIds, - buyer + DataTypes.ERC721SupplyParams[] + memory tokenData = new DataTypes.ERC721SupplyParams[]( + params.orderInfo.offer.length + ); + uint256[] memory tokenIds = new uint256[]( + params.orderInfo.offer.length ); + uint256 amountToSupply; + uint256 amountToCollateralize; + address payer; + + for (uint256 i = 0; i < params.orderInfo.offer.length; i++) { + OfferItem memory item = params.orderInfo.offer[i]; + require( + item.itemType == ItemType.ERC721, + Errors.INVALID_ASSET_TYPE + ); + require( + item.token == params.orderInfo.offer[0].token, + Errors.INVALID_MARKETPLACE_ORDER + ); + if (!vars.isCollectionListed) { + address owner = IERC721(vars.collectionToken).ownerOf( + item.identifierOrCriteria + ); + if (owner == address(this)) { + IERC721(vars.collectionToken).safeTransferFrom( + address(this), + buyer, + item.identifierOrCriteria + ); + } else { + require(owner == buyer, Errors.INVALID_MARKETPLACE_ORDER); + } + } else { + address nTokenOwner = IERC721(vars.collectionXTokenAddress) + .ownerOf(item.identifierOrCriteria); + if (nTokenOwner != address(0)) { + if (nTokenOwner == address(this)) { + IERC721(vars.collectionXTokenAddress).safeTransferFrom( + address(this), + buyer, + item.identifierOrCriteria + ); + } else { + require( + nTokenOwner == buyer, + Errors.INVALID_MARKETPLACE_ORDER + ); + } + tokenIds[amountToCollateralize++] = item + .identifierOrCriteria; + } else { + address owner = IERC721(vars.collectionToken).ownerOf( + item.identifierOrCriteria + ); + require( + owner == buyer || owner == address(this), + Errors.INVALID_MARKETPLACE_ORDER + ); + if (payer == address(0)) { + payer = owner; + } else { + require( + payer == owner, + Errors.INVALID_MARKETPLACE_ORDER + ); + } + tokenData[amountToSupply++] = DataTypes.ERC721SupplyParams( + item.identifierOrCriteria, + true + ); + } + } + } + + if (amountToSupply > 0) { + assembly { + mstore(tokenData, amountToSupply) + } + SupplyLogic.executeSupplyERC721( + ps._reserves, + ps._usersConfig[buyer], + DataTypes.ExecuteSupplyERC721Params({ + asset: vars.collectionToken, + tokenData: tokenData, + onBehalfOf: buyer, + payer: payer, + referralCode: 0 + }) + ); + } + + if (amountToCollateralize > 0) { + assembly { + mstore(tokenIds, amountToCollateralize) + } + SupplyExtendedLogic.executeCollateralizeERC721( + ps._reserves, + ps._usersConfig[buyer], + vars.collectionToken, + tokenIds, + buyer + ); + } } - function _validateAndGetPrice( + function _validateConsideration( DataTypes.ExecuteMarketplaceParams memory params, - MarketplaceLocalVars memory vars - ) internal pure returns (uint256 price) { - for (uint256 i = 0; i < params.orderInfo.consideration.length; i++) { + MarketplaceLocalVars memory vars, + address buyer + ) internal view returns (uint256 price, uint256 supplyAmount) { + uint256 size = params.orderInfo.consideration.length; + ConsiderationItem memory lastItem = params.orderInfo.consideration[ + size - 1 + ]; + ItemType requiredItemType = vars.isListingTokenETH + ? ItemType.NATIVE + : ItemType.ERC20; + + if (lastItem.itemType == ItemType.ERC721) { + require( + lastItem.recipient == buyer && --size > 0, + Errors.INVALID_MARKETPLACE_ORDER + ); + } + + for (uint256 i = 0; i < size; i++) { ConsiderationItem memory item = params.orderInfo.consideration[i]; require( item.startAmount == item.endAmount, Errors.INVALID_MARKETPLACE_ORDER ); require( - item.itemType == ItemType.ERC20 || - (vars.isETH && item.itemType == ItemType.NATIVE), + item.itemType == requiredItemType, Errors.INVALID_ASSET_TYPE ); require( - item.token == params.credit.token, - Errors.CREDIT_DOES_NOT_MATCH_ORDER + item.token == params.orderInfo.consideration[0].token, + Errors.INVALID_MARKETPLACE_ORDER ); price += item.startAmount; + + // supplyAmount is a **message** from the seller to the protocol + // to tell us the percentage of received funds to be supplied to be + // able to transfer NFT out + // + // NOTE: + // 1. this will only be useful in ParaSpace because listing on other platform + // will not be able to specify paraspace pool + // 2. paraspace pool as ERC20 recipient can avoid extra approval from seller + // side + if (item.recipient == address(this)) { + supplyAmount += item.startAmount; + } + } + } + + function _validateOffer( + DataTypes.PoolStorage storage ps, + DataTypes.ExecuteMarketplaceParams memory params, + MarketplaceLocalVars memory vars + ) internal view { + vars.collectionToken = params.orderInfo.offer[0].token; + vars.collectionXTokenAddress = ps + ._reserves[vars.collectionToken] + .xTokenAddress; + vars.isCollectionListed = vars.collectionXTokenAddress != address(0); + + if (!vars.isCollectionListed) { + try + INToken(vars.collectionToken).UNDERLYING_ASSET_ADDRESS() + returns (address underlyingAsset) { + bool isNToken = ps._reserves[underlyingAsset].xTokenAddress == + vars.collectionToken; + if (isNToken) { + vars.collectionXTokenAddress = vars.collectionToken; + vars.collectionToken = underlyingAsset; + vars.isCollectionListed = true; + } + } catch {} } + + require( + !vars.isCollectionListed || + INToken(vars.collectionXTokenAddress).getXTokenType() != + XTokenType.NTokenUniswapV3, + Errors.XTOKEN_TYPE_NOT_ALLOWED + ); } } diff --git a/contracts/protocol/libraries/logic/PositionMoverLogic.sol b/contracts/protocol/libraries/logic/PositionMoverLogic.sol index d73f39491..d9dbdb6f6 100644 --- a/contracts/protocol/libraries/logic/PositionMoverLogic.sol +++ b/contracts/protocol/libraries/logic/PositionMoverLogic.sol @@ -178,7 +178,13 @@ library PositionMoverLogic { reservesCount: ps._reservesCount, oracle: poolAddressProvider.getPriceOracle(), priceOracleSentinel: poolAddressProvider - .getPriceOracleSentinel() + .getPriceOracleSentinel(), + swapAdapter: DataTypes.SwapAdapter( + address(0), + address(0), + false + ), + swapPayload: bytes("") }) ); } @@ -401,7 +407,13 @@ library PositionMoverLogic { releaseUnderlying: false, reservesCount: params.reservesCount, oracle: params.priceOracle, - priceOracleSentinel: params.priceOracleSentinel + priceOracleSentinel: params.priceOracleSentinel, + swapAdapter: DataTypes.SwapAdapter( + address(0), + address(0), + false + ), + swapPayload: bytes("") }) ); } diff --git a/contracts/protocol/libraries/logic/SupplyLogic.sol b/contracts/protocol/libraries/logic/SupplyLogic.sol index 50d7f1c52..b70e0e506 100644 --- a/contracts/protocol/libraries/logic/SupplyLogic.sol +++ b/contracts/protocol/libraries/logic/SupplyLogic.sol @@ -445,8 +445,10 @@ library SupplyLogic { DataTypes.ReserveData storage reserve, DataTypes.ExecuteWithdrawERC721Params memory params ) internal returns (uint64, uint64) { - DataTypes.TimeLockParams memory timeLockParams = GenericLogic - .calculateTimeLockParams( + DataTypes.TimeLockParams memory timeLockParams; + + if (params.timeLock) { + timeLockParams = GenericLogic.calculateTimeLockParams( reserve, DataTypes.TimeLockFactorParams({ assetType: DataTypes.AssetType.ERC721, @@ -454,7 +456,8 @@ library SupplyLogic { amount: params.tokenIds.length }) ); - timeLockParams.actionType = DataTypes.TimeLockActionType.WITHDRAW; + timeLockParams.actionType = DataTypes.TimeLockActionType.WITHDRAW; + } return INToken(xTokenAddress).burn( diff --git a/contracts/protocol/libraries/logic/SwapLogic.sol b/contracts/protocol/libraries/logic/SwapLogic.sol new file mode 100644 index 000000000..ad1c7a025 --- /dev/null +++ b/contracts/protocol/libraries/logic/SwapLogic.sol @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IERC20} from "../../../dependencies/openzeppelin/contracts/IERC20.sol"; +import {GPv2SafeERC20} from "../../../dependencies/gnosis/contracts/GPv2SafeERC20.sol"; +import {IPToken} from "../../../interfaces/IPToken.sol"; +import {UserConfiguration} from "../configuration/UserConfiguration.sol"; +import {DataTypes} from "../types/DataTypes.sol"; +import {WadRayMath} from "../math/WadRayMath.sol"; +import {ValidationLogic} from "./ValidationLogic.sol"; +import {ReserveLogic} from "./ReserveLogic.sol"; +import {ISwapAdapter} from "../../../interfaces/ISwapAdapter.sol"; +import {SupplyLogic} from "./SupplyLogic.sol"; +import {BorrowLogic} from "./BorrowLogic.sol"; +import {IVariableDebtToken} from "../../../interfaces/IVariableDebtToken.sol"; +import {Helpers} from "../helpers/Helpers.sol"; + +/** + * @title SwapLogic library + * + * @notice Implements the xtoken swap logic + */ +library SwapLogic { + using ReserveLogic for DataTypes.ReserveData; + using GPv2SafeERC20 for IERC20; + using UserConfiguration for DataTypes.UserConfigurationMap; + using WadRayMath for uint256; + + event ReserveUsedAsCollateralDisabled( + address indexed reserve, + address indexed user + ); + event SwapPToken( + address indexed srcReserve, + address indexed dstReserve, + address indexed user, + uint256 srcAmount, + uint256 dstAmount + ); + event SwapDebt( + address indexed srcReserve, + address indexed dstReserve, + address indexed user, + uint256 srcAmount, + uint256 dstAmount + ); + + /** + * @notice Implements the ptoken swap feature. Through `atomicSwap()`, users redeem their xTokens for the underlying asset + * previously supplied in the ParaSpace protocol. + * @dev Emits the `Swap()` event. + * @dev If the user swap everything, `ReserveUsedAsCollateralDisabled()` is emitted. + * @param ps The pool storage pointer + * @param userConfig The user configuration mapping that tracks the supplied/borrowed assets + * @param params The additional parameters needed to execute the swap function + */ + function executeSwapPToken( + DataTypes.PoolStorage storage ps, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ExecuteSwapParams memory params + ) external { + DataTypes.ReserveData storage reserve = ps._reserves[params.srcAsset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + + reserve.updateState(reserveCache); + + uint256 userBalance = IPToken(reserveCache.xTokenAddress) + .scaledBalanceOf(msg.sender) + .rayMul(reserveCache.nextLiquidityIndex); + + uint256 amountToSwap = params.amount; + + ValidationLogic.validateWithdraw( + reserveCache, + amountToSwap, + userBalance + ); + + reserve.updateInterestRates( + reserveCache, + params.srcAsset, + 0, + amountToSwap + ); + + DataTypes.TimeLockParams memory timeLockParams; + DataTypes.SwapInfo memory swapInfo = ISwapAdapter( + params.swapAdapter.adapter + ).getSwapInfo(params.swapPayload, true); + ValidationLogic.validateSwap( + swapInfo, + DataTypes.ValidateSwapParams({ + swapAdapter: params.swapAdapter, + amount: amountToSwap, + srcToken: params.srcAsset, + dstToken: params.dstAsset, + dstReceiver: reserveCache.xTokenAddress + }) + ); + uint256 amountOut = IPToken(reserveCache.xTokenAddress).swapAndBurnFrom( + msg.sender, + address(this), + reserveCache.nextLiquidityIndex, + timeLockParams, + params.swapAdapter, + params.swapPayload, + swapInfo + ); + + SupplyLogic.executeSupply( + ps._reserves, + ps._usersConfig[params.user], + DataTypes.ExecuteSupplyParams({ + asset: params.dstAsset, + amount: amountOut, + onBehalfOf: params.user, + payer: address(this), + referralCode: 0 + }) + ); + + if (userConfig.isUsingAsCollateral(reserve.id)) { + Helpers.setAssetUsedAsCollateral( + ps._usersConfig[params.user], + ps._reserves, + params.dstAsset, + params.user + ); + + if (userConfig.isBorrowingAny()) { + ValidationLogic.validateHFAndLtvERC20( + ps._reserves, + ps._reservesList, + userConfig, + params.srcAsset, + msg.sender, + params.reservesCount, + params.oracle + ); + } + + if (amountToSwap == userBalance) { + userConfig.setUsingAsCollateral(reserve.id, false); + emit ReserveUsedAsCollateralDisabled( + params.srcAsset, + msg.sender + ); + } + } + + emit SwapPToken( + params.srcAsset, + params.dstAsset, + msg.sender, + amountToSwap, + amountOut + ); + } + + function executeSwapDebt( + DataTypes.PoolStorage storage ps, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ExecuteSwapParams memory params + ) external { + DataTypes.ReserveData storage reserve = ps._reserves[params.dstAsset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + + reserve.updateState(reserveCache); + + uint256 amountToSwap = params.amount; + uint256 variableDebt = Helpers.getUserCurrentDebt( + params.user, + ps._reserves[params.srcAsset].variableDebtTokenAddress + ); + + if (amountToSwap > variableDebt) { + amountToSwap = variableDebt; + } + + DataTypes.TimeLockParams memory timeLockParams; + DataTypes.SwapInfo memory swapInfo = ISwapAdapter( + params.swapAdapter.adapter + ).getSwapInfo(params.swapPayload, false); + ValidationLogic.validateSwap( + swapInfo, + DataTypes.ValidateSwapParams({ + swapAdapter: params.swapAdapter, + amount: amountToSwap, + srcToken: params.dstAsset, + dstToken: params.srcAsset, + dstReceiver: reserveCache.xTokenAddress + }) + ); + uint256 amountIn = IPToken(reserveCache.xTokenAddress) + .swapAndTransferUnderlyingTo( + address(this), + timeLockParams, + params.swapAdapter, + params.swapPayload, + swapInfo + ); + + BorrowLogic.executeRepay( + ps._reserves, + ps._usersConfig[params.user], + DataTypes.ExecuteRepayParams({ + asset: params.srcAsset, + amount: amountToSwap, + onBehalfOf: params.user, + payer: address(this), + usePTokens: false + }) + ); + + ValidationLogic.validateBorrow( + ps._reserves, + ps._reservesList, + DataTypes.ValidateBorrowParams({ + reserveCache: reserveCache, + userConfig: userConfig, + asset: params.dstAsset, + userAddress: params.user, + amount: amountIn, + reservesCount: params.reservesCount, + oracle: params.oracle, + priceOracleSentinel: params.priceOracleSentinel + }) + ); + + bool isFirstBorrowing = false; + ( + isFirstBorrowing, + reserveCache.nextScaledVariableDebt + ) = IVariableDebtToken(reserveCache.variableDebtTokenAddress).mint( + params.user, + params.user, + amountIn, + reserveCache.nextVariableBorrowIndex + ); + + reserve.updateInterestRates(reserveCache, params.dstAsset, 0, 0); + + if (isFirstBorrowing) { + userConfig.setBorrowing(reserve.id, true); + } + + emit SwapDebt( + params.srcAsset, + params.dstAsset, + params.user, + amountToSwap, + amountIn + ); + } +} diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 002bcba78..7529de5f6 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -1073,6 +1073,9 @@ library ValidationLogic { DataTypes.ExecuteMarketplaceParams memory params ) internal view { require(!params.marketplace.paused, Errors.MARKETPLACE_PAUSED); + if (params.credit.amount == 0) { + return; + } require( keccak256(abi.encodePacked(params.orderInfo.id)) == keccak256(abi.encodePacked(params.credit.orderId)), @@ -1090,6 +1093,12 @@ library ValidationLogic { ); } + function validateAcceptOpenseaBid( + DataTypes.ExecuteMarketplaceParams memory params + ) internal pure { + require(!params.marketplace.paused, Errors.MARKETPLACE_PAUSED); + } + function verifyCreditSignature( DataTypes.Credit memory credit, address signer, @@ -1210,4 +1219,31 @@ library ValidationLogic { ); } } + + function validateSwap( + DataTypes.SwapInfo memory swapInfo, + DataTypes.ValidateSwapParams memory params + ) internal pure { + require(!params.swapAdapter.paused, Errors.SWAP_ADAPTER_PAUSED); + require( + swapInfo.srcToken != swapInfo.dstToken, + Errors.INVALID_SWAP_PAYLOAD + ); + require( + (swapInfo.srcToken == params.srcToken && + (params.dstToken == address(0) || + swapInfo.dstToken == params.dstToken)), + Errors.INVALID_SWAP_PAYLOAD + ); + require( + (swapInfo.exactInput && swapInfo.maxAmountIn == params.amount) || + (!swapInfo.exactInput && + swapInfo.minAmountOut == params.amount), + Errors.INVALID_SWAP_PAYLOAD + ); + require( + swapInfo.dstReceiver == params.dstReceiver, + Errors.INVALID_SWAP_PAYLOAD + ); + } } diff --git a/contracts/protocol/libraries/types/DataTypes.sol b/contracts/protocol/libraries/types/DataTypes.sol index 0da8daba0..950e2de83 100644 --- a/contracts/protocol/libraries/types/DataTypes.sol +++ b/contracts/protocol/libraries/types/DataTypes.sol @@ -170,6 +170,8 @@ library DataTypes { uint256 reservesCount; address oracle; address priceOracleSentinel; + SwapAdapter swapAdapter; + bytes swapPayload; } struct ExecuteRepayParams { @@ -194,6 +196,7 @@ library DataTypes { address to; uint256 reservesCount; address oracle; + bool timeLock; } struct ExecuteDecreaseUniswapV3LiquidityParams { @@ -249,6 +252,14 @@ library DataTypes { address priceOracleSentinel; } + struct ValidateSwapParams { + SwapAdapter swapAdapter; + uint256 amount; + address srcToken; + address dstToken; + address dstReceiver; + } + struct ValidateLiquidateERC20Params { ReserveCache liquidationAssetReserveCache; address liquidationAsset; @@ -324,6 +335,18 @@ library DataTypes { bytes32 s; } + struct ExecuteSwapParams { + address srcAsset; + address dstAsset; + uint256 amount; + address user; + uint256 reservesCount; + address oracle; + address priceOracleSentinel; + SwapAdapter swapAdapter; + bytes swapPayload; + } + struct ExecuteMarketplaceParams { bytes32 marketplaceId; bytes payload; @@ -332,10 +355,11 @@ library DataTypes { DataTypes.Marketplace marketplace; OrderInfo orderInfo; address weth; - uint16 referralCode; uint256 reservesCount; address oracle; address priceOracleSentinel; + SwapAdapter swapAdapter; + bytes swapPayload; } struct OrderInfo { @@ -344,6 +368,17 @@ library DataTypes { bytes id; OfferItem[] offer; ConsiderationItem[] consideration; + bool isSeaport; + } + + struct SwapInfo { + address srcToken; + address dstToken; + address srcReceiver; + address dstReceiver; + uint256 maxAmountIn; + uint256 minAmountOut; + bool exactInput; } struct Marketplace { @@ -353,6 +388,12 @@ library DataTypes { bool paused; } + struct SwapAdapter { + address adapter; + address router; + bool paused; + } + struct Auction { uint256 startTime; } @@ -406,6 +447,8 @@ library DataTypes { uint16 _apeCompoundFee; // Map of user's ape compound strategies mapping(address => ApeCompoundStrategy) _apeCompoundStrategies; + // Map of swap adapters + mapping(bytes32 => DataTypes.SwapAdapter) _swapAdapters; } struct ReserveConfigData { diff --git a/contracts/protocol/pool/PoolApeStaking.sol b/contracts/protocol/pool/PoolApeStaking.sol index 487cc19c2..69f2ba935 100644 --- a/contracts/protocol/pool/PoolApeStaking.sol +++ b/contracts/protocol/pool/PoolApeStaking.sol @@ -380,7 +380,13 @@ contract PoolApeStaking is reservesCount: ps._reservesCount, oracle: ADDRESSES_PROVIDER.getPriceOracle(), priceOracleSentinel: ADDRESSES_PROVIDER - .getPriceOracleSentinel() + .getPriceOracleSentinel(), + swapAdapter: DataTypes.SwapAdapter( + address(0), + address(0), + false + ), + swapPayload: bytes("") }) ); } diff --git a/contracts/protocol/pool/PoolBorrowAndStake.sol b/contracts/protocol/pool/PoolBorrowAndStake.sol index 1af514995..25baabbf3 100644 --- a/contracts/protocol/pool/PoolBorrowAndStake.sol +++ b/contracts/protocol/pool/PoolBorrowAndStake.sol @@ -263,7 +263,13 @@ contract PoolBorrowAndStake is reservesCount: ps._reservesCount, oracle: ADDRESSES_PROVIDER.getPriceOracle(), priceOracleSentinel: ADDRESSES_PROVIDER - .getPriceOracleSentinel() + .getPriceOracleSentinel(), + swapAdapter: DataTypes.SwapAdapter( + address(0), + address(0), + false + ), + swapPayload: bytes("") }) ); } diff --git a/contracts/protocol/pool/PoolCore.sol b/contracts/protocol/pool/PoolCore.sol index c664e09ab..be2fa317a 100644 --- a/contracts/protocol/pool/PoolCore.sol +++ b/contracts/protocol/pool/PoolCore.sol @@ -11,6 +11,7 @@ import {SupplyExtendedLogic} from "../libraries/logic/SupplyExtendedLogic.sol"; import {MarketplaceLogic} from "../libraries/logic/MarketplaceLogic.sol"; import {BorrowLogic} from "../libraries/logic/BorrowLogic.sol"; import {LiquidationLogic} from "../libraries/logic/LiquidationLogic.sol"; +import {SwapLogic} from "../libraries/logic/SwapLogic.sol"; import {AuctionLogic} from "../libraries/logic/AuctionLogic.sol"; import {DataTypes} from "../libraries/types/DataTypes.sol"; import {IERC20WithPermit} from "../../interfaces/IERC20WithPermit.sol"; @@ -231,7 +232,8 @@ contract PoolCore is tokenIds: tokenIds, to: to, reservesCount: ps._reservesCount, - oracle: ADDRESSES_PROVIDER.getPriceOracle() + oracle: ADDRESSES_PROVIDER.getPriceOracle(), + timeLock: true }) ); } @@ -287,7 +289,46 @@ contract PoolCore is releaseUnderlying: true, reservesCount: ps._reservesCount, oracle: ADDRESSES_PROVIDER.getPriceOracle(), - priceOracleSentinel: ADDRESSES_PROVIDER.getPriceOracleSentinel() + priceOracleSentinel: ADDRESSES_PROVIDER + .getPriceOracleSentinel(), + swapAdapter: DataTypes.SwapAdapter( + address(0), + address(0), + false + ), + swapPayload: bytes("") + }) + ); + } + + /// @inheritdoc IPoolCore + function borrowAny( + address asset, + uint256 amount, + uint16 referralCode, + address onBehalfOf, + bytes32 swapAdapterId, + bytes calldata swapPayload + ) external virtual override nonReentrant { + DataTypes.PoolStorage storage ps = poolStorage(); + + BorrowLogic.executeBorrow( + ps._reserves, + ps._reservesList, + ps._usersConfig[onBehalfOf], + DataTypes.ExecuteBorrowParams({ + asset: asset, + user: msg.sender, + onBehalfOf: onBehalfOf, + amount: amount, + referralCode: referralCode, + releaseUnderlying: true, + reservesCount: ps._reservesCount, + oracle: ADDRESSES_PROVIDER.getPriceOracle(), + priceOracleSentinel: ADDRESSES_PROVIDER + .getPriceOracleSentinel(), + swapAdapter: ps._swapAdapters[swapAdapterId], + swapPayload: swapPayload }) ); } @@ -486,6 +527,63 @@ contract PoolCore is ); } + /// @inheritdoc IPoolCore + function swapPToken( + address srcAsset, + uint256 srcAmount, + address dstAsset, + address to, + bytes32 swapAdapterId, + bytes calldata swapPayload + ) external nonReentrant { + DataTypes.PoolStorage storage ps = poolStorage(); + + SwapLogic.executeSwapPToken( + ps, + ps._usersConfig[msg.sender], + DataTypes.ExecuteSwapParams({ + srcAsset: srcAsset, + dstAsset: dstAsset, + amount: srcAmount, + user: to, + reservesCount: ps._reservesCount, + oracle: ADDRESSES_PROVIDER.getPriceOracle(), + priceOracleSentinel: ADDRESSES_PROVIDER + .getPriceOracleSentinel(), + swapAdapter: ps._swapAdapters[swapAdapterId], + swapPayload: swapPayload + }) + ); + } + + /// @inheritdoc IPoolCore + function swapDebt( + address srcAsset, + uint256 srcAmount, + address dstAsset, + bytes32 swapAdapterId, + bytes calldata swapPayload + ) external nonReentrant { + DataTypes.PoolStorage storage ps = poolStorage(); + + SwapLogic.executeSwapDebt( + ps, + ps._usersConfig[msg.sender], + DataTypes.ExecuteSwapParams({ + srcAsset: srcAsset, + dstAsset: dstAsset, + amount: srcAmount, + user: msg.sender, + reservesCount: ps._reservesCount, + oracle: ADDRESSES_PROVIDER.getPriceOracle(), + priceOracleSentinel: ADDRESSES_PROVIDER + .getPriceOracleSentinel(), + swapAdapter: ps._swapAdapters[swapAdapterId], + swapPayload: swapPayload + }) + ); + } + /// @inheritdoc IPoolCore function startAuction( address user, diff --git a/contracts/protocol/pool/PoolMarketplace.sol b/contracts/protocol/pool/PoolMarketplace.sol index 23aad7444..b489462d1 100644 --- a/contracts/protocol/pool/PoolMarketplace.sol +++ b/contracts/protocol/pool/PoolMarketplace.sol @@ -2,33 +2,13 @@ pragma solidity ^0.8.0; import {ParaVersionedInitializable} from "../libraries/paraspace-upgradeability/ParaVersionedInitializable.sol"; -import {Errors} from "../libraries/helpers/Errors.sol"; -import {ReserveConfiguration} from "../libraries/configuration/ReserveConfiguration.sol"; -import {PoolLogic} from "../libraries/logic/PoolLogic.sol"; -import {ReserveLogic} from "../libraries/logic/ReserveLogic.sol"; -import {SupplyLogic} from "../libraries/logic/SupplyLogic.sol"; import {MarketplaceLogic} from "../libraries/logic/MarketplaceLogic.sol"; -import {BorrowLogic} from "../libraries/logic/BorrowLogic.sol"; -import {LiquidationLogic} from "../libraries/logic/LiquidationLogic.sol"; import {DataTypes} from "../libraries/types/DataTypes.sol"; -import {IERC20WithPermit} from "../../interfaces/IERC20WithPermit.sol"; -import {IERC20} from "../../dependencies/openzeppelin/contracts/IERC20.sol"; -import {SafeERC20} from "../../dependencies/openzeppelin/contracts/SafeERC20.sol"; -import {IWETH} from "../../misc/interfaces/IWETH.sol"; -import {ItemType} from "../../dependencies/seaport/contracts/lib/ConsiderationEnums.sol"; import {IPoolAddressesProvider} from "../../interfaces/IPoolAddressesProvider.sol"; import {IPoolMarketplace} from "../../interfaces/IPoolMarketplace.sol"; -import {INToken} from "../../interfaces/INToken.sol"; -import {IACLManager} from "../../interfaces/IACLManager.sol"; import {PoolStorage} from "./PoolStorage.sol"; -import {FlashClaimLogic} from "../libraries/logic/FlashClaimLogic.sol"; import {Address} from "../../dependencies/openzeppelin/contracts/Address.sol"; -import {IERC721Receiver} from "../../dependencies/openzeppelin/contracts/IERC721Receiver.sol"; -import {IMarketplace} from "../../interfaces/IMarketplace.sol"; -import {Errors} from "../libraries/helpers/Errors.sol"; import {ParaReentrancyGuard} from "../libraries/paraspace-upgradeability/ParaReentrancyGuard.sol"; -import {IAuctionableERC721} from "../../interfaces/IAuctionableERC721.sol"; -import {IReserveAuctionStrategy} from "../../interfaces/IReserveAuctionStrategy.sol"; /** * @title Pool Marketplace contract @@ -49,9 +29,6 @@ contract PoolMarketplace is PoolStorage, IPoolMarketplace { - using ReserveLogic for DataTypes.ReserveData; - using SafeERC20 for IERC20; - IPoolAddressesProvider internal immutable ADDRESSES_PROVIDER; uint256 internal constant POOL_REVISION = 200; @@ -69,10 +46,30 @@ contract PoolMarketplace is /// @inheritdoc IPoolMarketplace function buyWithCredit( + bytes32 marketplaceId, + bytes calldata payload, + DataTypes.Credit calldata credit + ) external payable virtual override nonReentrant { + DataTypes.PoolStorage storage ps = poolStorage(); + + MarketplaceLogic.executeBuyWithCredit( + ps, + marketplaceId, + payload, + credit, + DataTypes.SwapAdapter(address(0), address(0), false), + bytes(""), + ADDRESSES_PROVIDER + ); + } + + /// @inheritdoc IPoolMarketplace + function buyAnyWithCredit( bytes32 marketplaceId, bytes calldata payload, DataTypes.Credit calldata credit, - uint16 referralCode + bytes32 swapAdapterId, + bytes calldata swapPayload ) external payable virtual override nonReentrant { DataTypes.PoolStorage storage ps = poolStorage(); @@ -81,17 +78,38 @@ contract PoolMarketplace is marketplaceId, payload, credit, - ADDRESSES_PROVIDER, - referralCode + ps._swapAdapters[swapAdapterId], + swapPayload, + ADDRESSES_PROVIDER ); } /// @inheritdoc IPoolMarketplace function batchBuyWithCredit( + bytes32[] calldata marketplaceIds, + bytes[] calldata payloads, + DataTypes.Credit[] calldata credits + ) external payable virtual override nonReentrant { + DataTypes.PoolStorage storage ps = poolStorage(); + + MarketplaceLogic.executeBatchBuyWithCredit( + ps, + marketplaceIds, + payloads, + credits, + new DataTypes.SwapAdapter[](credits.length), + new bytes[](credits.length), + ADDRESSES_PROVIDER + ); + } + + /// @inheritdoc IPoolMarketplace + function batchBuyAnyWithCredit( bytes32[] calldata marketplaceIds, bytes[] calldata payloads, DataTypes.Credit[] calldata credits, - uint16 referralCode + DataTypes.SwapAdapter[] calldata swapAdapters, + bytes[] calldata swapPayloads ) external payable virtual override nonReentrant { DataTypes.PoolStorage storage ps = poolStorage(); @@ -100,8 +118,9 @@ contract PoolMarketplace is marketplaceIds, payloads, credits, - ADDRESSES_PROVIDER, - referralCode + swapAdapters, + swapPayloads, + ADDRESSES_PROVIDER ); } @@ -110,8 +129,7 @@ contract PoolMarketplace is bytes32 marketplaceId, bytes calldata payload, DataTypes.Credit calldata credit, - address onBehalfOf, - uint16 referralCode + address onBehalfOf ) external virtual override nonReentrant { DataTypes.PoolStorage storage ps = poolStorage(); @@ -121,8 +139,7 @@ contract PoolMarketplace is payload, credit, onBehalfOf, - ADDRESSES_PROVIDER, - referralCode + ADDRESSES_PROVIDER ); } @@ -131,8 +148,7 @@ contract PoolMarketplace is bytes32[] calldata marketplaceIds, bytes[] calldata payloads, DataTypes.Credit[] calldata credits, - address onBehalfOf, - uint16 referralCode + address onBehalfOf ) external virtual override nonReentrant { DataTypes.PoolStorage storage ps = poolStorage(); @@ -142,17 +158,41 @@ contract PoolMarketplace is payloads, credits, onBehalfOf, - ADDRESSES_PROVIDER, - referralCode + ADDRESSES_PROVIDER + ); + } + + /// @inheritdoc IPoolMarketplace + function acceptOpenseaBid( + bytes32 marketplaceId, + bytes calldata payload, + address onBehalfOf + ) external virtual override nonReentrant { + DataTypes.PoolStorage storage ps = poolStorage(); + + MarketplaceLogic.executeAcceptOpenseaBid( + ps, + marketplaceId, + payload, + onBehalfOf, + ADDRESSES_PROVIDER ); } - // function movePositionFromBendDAO(uint256[] calldata loanIds) external nonReentrant { - // DataTypes.PoolStorage storage ps = poolStorage(); + /// @inheritdoc IPoolMarketplace + function batchAcceptOpenseaBid( + bytes32[] calldata marketplaceIds, + bytes[] calldata payloads, + address onBehalfOf + ) external virtual override nonReentrant { + DataTypes.PoolStorage storage ps = poolStorage(); - // PositionMoverLogic.executeMovePositionFromBendDAO( - // ps, - // ADDRESSES_PROVIDER - // ); - // } + MarketplaceLogic.executeBatchAcceptOpenseaBid( + ps, + marketplaceIds, + payloads, + onBehalfOf, + ADDRESSES_PROVIDER + ); + } } diff --git a/contracts/protocol/pool/PoolParameters.sol b/contracts/protocol/pool/PoolParameters.sol index a559a4e4d..e8de3cbc2 100644 --- a/contracts/protocol/pool/PoolParameters.sol +++ b/contracts/protocol/pool/PoolParameters.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.10; +pragma solidity ^0.8.0; import {ParaVersionedInitializable} from "../libraries/paraspace-upgradeability/ParaVersionedInitializable.sol"; import {Errors} from "../libraries/helpers/Errors.sol"; @@ -269,6 +269,29 @@ contract PoolParameters is strategy = ps._apeCompoundStrategies[user]; } + /// @inheritdoc IPoolParameters + function setSwapAdapter( + bytes32 swapAdapterId, + DataTypes.SwapAdapter calldata adapter + ) external { + DataTypes.PoolStorage storage ps = poolStorage(); + ps._swapAdapters[swapAdapterId] = adapter; + emit SwapAdapterUpdated( + swapAdapterId, + adapter.adapter, + adapter.router, + adapter.paused + ); + } + + /// @inheritdoc IPoolParameters + function getSwapAdapter( + bytes32 swapAdapterId + ) external view returns (DataTypes.SwapAdapter memory adapter) { + DataTypes.PoolStorage storage ps = poolStorage(); + adapter = ps._swapAdapters[swapAdapterId]; + } + /// @inheritdoc IPoolParameters function setAuctionRecoveryHealthFactor( uint64 value diff --git a/contracts/protocol/tokenization/PToken.sol b/contracts/protocol/tokenization/PToken.sol index 05277b37d..21cdd2863 100644 --- a/contracts/protocol/tokenization/PToken.sol +++ b/contracts/protocol/tokenization/PToken.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.0; import {IERC20} from "../../dependencies/openzeppelin/contracts/IERC20.sol"; -import {GPv2SafeERC20} from "../../dependencies/gnosis/contracts/GPv2SafeERC20.sol"; import {SafeCast} from "../../dependencies/openzeppelin/contracts/SafeCast.sol"; +import {SafeERC20} from "../../dependencies/openzeppelin/contracts/SafeERC20.sol"; import {VersionedInitializable} from "../libraries/paraspace-upgradeability/VersionedInitializable.sol"; import {Errors} from "../libraries/helpers/Errors.sol"; import {WadRayMath} from "../libraries/math/WadRayMath.sol"; @@ -17,6 +17,10 @@ import {EIP712Base} from "./base/EIP712Base.sol"; import {XTokenType} from "../../interfaces/IXTokenType.sol"; import {ITimeLock} from "../../interfaces/ITimeLock.sol"; import {DataTypes} from "../libraries/types/DataTypes.sol"; +import {Address} from "../../dependencies/openzeppelin/contracts/Address.sol"; +import {ISwapAdapter} from "../../interfaces/ISwapAdapter.sol"; +import {Helpers} from "../../protocol/libraries/helpers/Helpers.sol"; +import {Math} from "../../dependencies/openzeppelin/contracts/Math.sol"; import {ICApe} from "../../interfaces/ICApe.sol"; import {IAutoCompoundApe} from "../../interfaces/IAutoCompoundApe.sol"; @@ -33,7 +37,7 @@ contract PToken is { using WadRayMath for uint256; using SafeCast for uint256; - using GPv2SafeERC20 for IERC20; + using SafeERC20 for IERC20; bytes32 public constant PERMIT_TYPEHASH = keccak256( @@ -119,25 +123,44 @@ contract PToken is ) external virtual override onlyPool { _burnScaled(from, receiverOfUnderlying, amount, index); if (receiverOfUnderlying != address(this)) { - if (timeLockParams.releaseTime != 0) { - ITimeLock timeLock = POOL.TIME_LOCK(); - uint256[] memory amounts = new uint256[](1); - amounts[0] = amount; - - timeLock.createAgreement( - DataTypes.AssetType.ERC20, - timeLockParams.actionType, - _underlyingAsset, - amounts, - receiverOfUnderlying, - timeLockParams.releaseTime - ); - receiverOfUnderlying = address(timeLock); - } - IERC20(_underlyingAsset).safeTransfer(receiverOfUnderlying, amount); + _sendToUserOrTimeLock( + timeLockParams, + POOL.TIME_LOCK(), + _underlyingAsset, + amount, + receiverOfUnderlying + ); } } + function swapAndBurnFrom( + address from, + address receiverOfUnderlying, + uint256 index, + DataTypes.TimeLockParams calldata timeLockParams, + DataTypes.SwapAdapter calldata swapAdapter, + bytes calldata swapPayload, + DataTypes.SwapInfo calldata swapInfo + ) external virtual override onlyPool returns (uint256 amount) { + require( + receiverOfUnderlying != address(this), + Errors.INVALID_RECIPIENT + ); + amount = swapAndTransferUnderlyingTo( + receiverOfUnderlying, + timeLockParams, + swapAdapter, + swapPayload, + swapInfo + ); + _burnScaled( + from, + receiverOfUnderlying, + swapInfo.exactInput ? swapInfo.maxAmountIn : amount, + index + ); + } + /// @inheritdoc IPToken function mintToTreasury( uint256 amount, @@ -219,31 +242,55 @@ contract PToken is address target, uint256 amount, DataTypes.TimeLockParams calldata timeLockParams - ) external virtual override onlyPool { - if (timeLockParams.releaseTime != 0) { - ITimeLock timeLock = POOL.TIME_LOCK(); - uint256[] memory amounts = new uint256[](1); - amounts[0] = amount; - - timeLock.createAgreement( - DataTypes.AssetType.ERC20, - timeLockParams.actionType, - _underlyingAsset, - amounts, - target, - timeLockParams.releaseTime - ); - target = address(timeLock); - } - IERC20(_underlyingAsset).safeTransfer(target, amount); + ) public virtual override onlyPool { + _sendToUserOrTimeLock( + timeLockParams, + POOL.TIME_LOCK(), + _underlyingAsset, + amount, + target + ); } /// @inheritdoc IPToken - function handleRepayment( - address user, - uint256 amount - ) external virtual override onlyPool { - // Intentionally left blank + function swapAndTransferUnderlyingTo( + address target, + DataTypes.TimeLockParams calldata timeLockParams, + DataTypes.SwapAdapter calldata swapAdapter, + bytes calldata swapPayload, + DataTypes.SwapInfo calldata swapInfo + ) public virtual override onlyPool returns (uint256 amount) { + IERC20(swapInfo.srcToken).safeApprove( + swapAdapter.router, + swapInfo.maxAmountIn + ); + + bytes memory returndata = Address.functionDelegateCall( + swapAdapter.adapter, + abi.encodeWithSelector( + ISwapAdapter.swap.selector, + swapAdapter.router, + swapPayload, + swapInfo.exactInput + ) + ); + amount = abi.decode(returndata, (uint256)); + + uint256 amountOut = swapInfo.exactInput + ? amount + : swapInfo.minAmountOut; + + require(amountOut > 0, Errors.CALL_SWAP_FAILED); + + _sendToUserOrTimeLock( + timeLockParams, + POOL.TIME_LOCK(), + swapInfo.dstToken, + amountOut, + target + ); + + IERC20(swapInfo.srcToken).safeApprove(swapAdapter.router, 0); } /// @inheritdoc IPToken @@ -381,6 +428,31 @@ contract PToken is return XTokenType.PToken; } + function _sendToUserOrTimeLock( + DataTypes.TimeLockParams calldata timeLockParams, + ITimeLock timeLock, + address asset, + uint256 amount, + address target + ) internal { + if (timeLockParams.releaseTime != 0) { + uint256[] memory amounts = new uint256[](1); + amounts[0] = amount; + + timeLock.createAgreement( + DataTypes.AssetType.ERC20, + timeLockParams.actionType, + asset, + amounts, + target, + timeLockParams.releaseTime + ); + + target = address(timeLock); + } + IERC20(asset).safeTransfer(target, amount); + } + function claimUnderlying( address timeLockV1, address cApeV1, diff --git a/contracts/ui/WPunkGateway.sol b/contracts/ui/WPunkGateway.sol index bcf58fd2b..3afb72de2 100644 --- a/contracts/ui/WPunkGateway.sol +++ b/contracts/ui/WPunkGateway.sol @@ -123,14 +123,12 @@ contract WPunkGateway is * @param marketplaceId The marketplace identifier * @param payload The encoded parameters to be passed to marketplace contract (selector eliminated) * @param credit The credit that user would like to use for this purchase - * @param referralCode The referral code used */ function acceptBidWithCredit( bytes32 marketplaceId, bytes calldata payload, DataTypes.Credit calldata credit, - uint256[] calldata punkIndexes, - uint16 referralCode + uint256[] calldata punkIndexes ) external nonReentrant { for (uint256 i = 0; i < punkIndexes.length; i++) { address punkOwner = Punk.punkIndexToAddress(punkIndexes[i]); @@ -147,13 +145,7 @@ contract WPunkGateway is punkIndexes[i] ); } - Pool.acceptBidWithCredit( - marketplaceId, - payload, - credit, - msg.sender, - referralCode - ); + Pool.acceptBidWithCredit(marketplaceId, payload, credit, msg.sender); } /** @@ -164,14 +156,12 @@ contract WPunkGateway is * @param marketplaceIds The marketplace identifiers * @param payloads The encoded parameters to be passed to marketplace contract (selector eliminated) * @param credits The credits that the makers have approved to use for this purchase - * @param referralCode The referral code used */ function batchAcceptBidWithCredit( bytes32[] calldata marketplaceIds, bytes[] calldata payloads, DataTypes.Credit[] calldata credits, - uint256[] calldata punkIndexes, - uint16 referralCode + uint256[] calldata punkIndexes ) external nonReentrant { for (uint256 i = 0; i < punkIndexes.length; i++) { address punkOwner = Punk.punkIndexToAddress(punkIndexes[i]); @@ -192,8 +182,7 @@ contract WPunkGateway is marketplaceIds, payloads, credits, - msg.sender, - referralCode + msg.sender ); } diff --git a/contracts/ui/interfaces/IWPunkGateway.sol b/contracts/ui/interfaces/IWPunkGateway.sol index 37eb6d935..f7a75c066 100644 --- a/contracts/ui/interfaces/IWPunkGateway.sol +++ b/contracts/ui/interfaces/IWPunkGateway.sol @@ -24,15 +24,13 @@ interface IWPunkGateway { bytes32 marketplaceId, bytes calldata payload, DataTypes.Credit calldata credit, - uint256[] calldata punkIndexes, - uint16 referralCode + uint256[] calldata punkIndexes ) external; function batchAcceptBidWithCredit( bytes32[] calldata marketplaceIds, bytes[] calldata payloads, DataTypes.Credit[] calldata credits, - uint256[] calldata punkIndexes, - uint16 referralCode + uint256[] calldata punkIndexes ) external; } diff --git a/helpers/constants.ts b/helpers/constants.ts index a3f8824c6..bd594e84b 100644 --- a/helpers/constants.ts +++ b/helpers/constants.ts @@ -29,11 +29,21 @@ export const SAPE_ADDRESS = ONE_ADDRESS; // ---------------- // MARKETPLACE // ---------------- -export const OPENSEA_SEAPORT_ID = solidityKeccak256( +export const OPENSEA_SEAPORT_ID_V11 = solidityKeccak256( + ["string"], + ["Opensea/seaport/v1.1"] +); + +export const OPENSEA_SEAPORT_ID_V15 = solidityKeccak256( ["string"], ["Opensea/seaport/v1.5"] ); +export const OPENSEA_SEAPORT_ID_V14 = solidityKeccak256( + ["string"], + ["Opensea/seaport/v1.4"] +); + export const PARASPACE_SEAPORT_ID = solidityKeccak256( ["string"], ["ParaSpace/seaport/v1.1"] @@ -42,3 +52,8 @@ export const PARASPACE_SEAPORT_ID = solidityKeccak256( export const LOOKSRARE_ID = solidityKeccak256(["string"], ["LooksRare/v1.1"]); export const X2Y2_ID = solidityKeccak256(["string"], ["X2Y2/v1"]); export const BLUR_ID = solidityKeccak256(["string"], ["Blur/v1"]); + +export const UNISWAP_V3_SWAP_ADAPTER_ID = solidityKeccak256( + ["string"], + ["SwapAdapter/Uniswap/v3"] +); diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 16cb563ae..8f910bdb7 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -109,50 +109,14 @@ import { PoolPositionMover, PoolPositionMover__factory, PositionMoverLogic, - PriceOracle, - ProtocolDataProvider, - PToken, - PTokenAStETH, - PTokenAToken, - PTokenCApe, - PTokenSApe, - PTokenStETH, - PTokenStKSM, - PYieldToken, - ReservesSetupHelper, - RoyaltyFeeManager, - RoyaltyFeeRegistry, - Seaport, - SeaportAdapter, - StakefishNFTManager, - StakefishValidatorFactory, - StakefishValidatorV1, - StandardPolicyERC721, - StETHDebtToken, - StETHMocked, - StKSMDebtToken, - StrategyStandardSaleForFixedPrice, - SupplyExtendedLogic, - SupplyLogic, + PositionMoverLogic__factory, + UniswapV3SwapAdapter__factory, + UniswapV3SwapAdapter, TimeLock, - TransferManagerERC1155, - TransferManagerERC721, - TransferSelectorNFT, - UiIncentiveDataProvider, - UiPoolDataProvider, - UniswapV3Factory, - UniswapV3OracleWrapper, - UniswapV3TwapOracleWrapper, - UserFlashclaimRegistry, - VariableDebtToken, - WalletBalanceProvider, - WETH9Mocked, - WETHGateway, - WPunk, - WPunkGateway, - WstETHMocked, - X2Y2Adapter, - X2Y2R1, + NTokenChromieSquiggle__factory, + CLFixedPriceSynchronicityPriceAdapter, + CLFixedPriceSynchronicityPriceAdapter__factory, + SwapLogic, PoolBorrowAndStake__factory, PoolBorrowAndStake, } from "../types"; @@ -195,6 +159,10 @@ import { import * as nFTDescriptor from "@uniswap/v3-periphery/artifacts/contracts/libraries/NFTDescriptor.sol/NFTDescriptor.json"; import * as nonfungibleTokenPositionDescriptor from "@uniswap/v3-periphery/artifacts/contracts/NonfungibleTokenPositionDescriptor.sol/NonfungibleTokenPositionDescriptor.json"; import {Contract} from "ethers"; +import { + SwapLogicLibraryAddresses, + SwapLogic__factory, +} from "../types/factories/contracts/protocol/libraries/logic/SwapLogic__factory"; import {Address, Libraries} from "hardhat-deploy/dist/types"; import {parseEther} from "ethers/lib/utils"; @@ -449,6 +417,15 @@ export const deployPoolCoreLibraries = async ( verify ); const flashClaimLogic = await deployFlashClaimLogic(verify); + const swapLogic = await deploySwapLogic( + { + ["contracts/protocol/libraries/logic/SupplyLogic.sol:SupplyLogic"]: + supplyLogic.address, + ["contracts/protocol/libraries/logic/BorrowLogic.sol:BorrowLogic"]: + borrowLogic.address, + }, + verify + ); return { ["contracts/protocol/libraries/logic/AuctionLogic.sol:AuctionLogic"]: @@ -463,6 +440,8 @@ export const deployPoolCoreLibraries = async ( borrowLogic.address, ["contracts/protocol/libraries/logic/FlashClaimLogic.sol:FlashClaimLogic"]: flashClaimLogic.address, + ["contracts/protocol/libraries/logic/SwapLogic.sol:SwapLogic"]: + swapLogic.address, }; }; @@ -1902,6 +1881,22 @@ export const deployMarketplaceLogic = async ( ) as Promise; }; +export const deploySwapLogic = async ( + libraries: SwapLogicLibraryAddresses, + verify?: boolean +) => { + const swapLogic = new SwapLogic__factory(libraries, await getFirstSigner()); + + return withSaveAndVerify( + swapLogic, + eContractid.SwapLogic, + [], + verify, + false, + libraries + ) as Promise; +}; + export const deployConduitController = async (verify?: boolean) => withSaveAndVerify( await getContractFactory("ConduitController"), @@ -3193,6 +3188,14 @@ export const deployStakefishValidator = async ( verify ) as Promise; +export const deployUniswapV3SwapAdapter = async (verify?: boolean) => + withSaveAndVerify( + new UniswapV3SwapAdapter__factory(await getFirstSigner()), + eContractid.UniswapV3SwapAdapter, + [], + verify + ) as Promise; + export const deployAccount = async ( entryPoint: tEthereumAddress, verify?: boolean diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index 1e9e26c93..72ec35803 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -82,6 +82,7 @@ import { NTokenOtherdeed__factory, TimeLock__factory, P2PPairStaking__factory, + ISwapRouter__factory, NFTFloorOracle__factory, } from "../types"; import { @@ -1074,6 +1075,7 @@ export const decodeInputData = (data: string) => { ...NTokenOtherdeed__factory.abi, ...TimeLock__factory.abi, ...P2PPairStaking__factory.abi, + ...ISwapRouter__factory.abi, ...NFTFloorOracle__factory.abi, ]; diff --git a/helpers/types.ts b/helpers/types.ts index 309d65b2f..fe1834cd5 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -204,6 +204,7 @@ export enum eContractid { PausableZone = "PausableZone", Seaport = "Seaport", MarketplaceLogic = "MarketplaceLogic", + SwapLogic = "SwapLogic", SeaportAdapter = "SeaportAdapter", LooksRareAdapter = "LooksRareAdapter", X2Y2Adapter = "X2Y2Adapter", @@ -291,6 +292,7 @@ export enum eContractid { MockBendDaoLendPool = "MockBendDaoLendPool", PositionMoverLogic = "PositionMoverLogic", PoolPositionMoverImpl = "PoolPositionMoverImpl", + UniswapV3SwapAdapter = "UniswapV3SwapAdapter", Account = "Account", AccountFactory = "AccountFactory", AccountProxy = "AccountProxy", @@ -299,6 +301,8 @@ export enum eContractid { /* * Error messages + * + * generated using vim regex: :s/string public constant \(.*\) = "\(.*\)";/ \1 = "\2", */ export enum ProtocolErrors { CALLER_NOT_POOL_ADMIN = "1", // 'The caller of the function is not a pool admin' @@ -311,7 +315,7 @@ export enum ProtocolErrors { INVALID_ADDRESSES_PROVIDER_ID = "8", // 'Invalid id for the pool addresses provider' NOT_CONTRACT = "9", // 'Address is not a contract' CALLER_NOT_POOL_CONFIGURATOR = "10", // 'The caller of the function is not the pool configurator' - CALLER_NOT_XTOKEN = "11", // 'The caller of the function is not an PToken' + CALLER_NOT_XTOKEN = "11", // 'The caller of the function is not an PToken or NToken' INVALID_ADDRESSES_PROVIDER = "12", // 'The address of the pool addresses provider is invalid' RESERVE_ALREADY_ADDED = "14", // 'Reserve has already been added to reserve list' NO_MORE_RESERVES_ALLOWED = "15", // 'Maximum amount of reserves in the pool reached' @@ -401,21 +405,34 @@ export enum ProtocolErrors { AUCTION_NOT_ENABLED = "113", //auction not enabled on the reserve. ERC721_HEALTH_FACTOR_NOT_ABOVE_THRESHOLD = "114", //ERC721 Health factor is not above the threshold. TOKEN_IN_AUCTION = "115", //tokenId is in auction. - AUCTIONED_BALANCE_NOT_ZERO = "116", //auctioned balance not zero - - LIQUIDATOR_CAN_NOT_BE_SELF = "117", //user can not liquidate himself - FLASHCLAIM_NOT_ALLOWED = "119", //flash claim is not allowed for UniswapV3 + AUCTIONED_BALANCE_NOT_ZERO = "116", //auctioned balance not zero. + LIQUIDATOR_CAN_NOT_BE_SELF = "117", //user can not liquidate himself. + INVALID_RECIPIENT = "118", //invalid recipient specified in order. + FLASHCLAIM_NOT_ALLOWED = "119", //flash claim is not allowed for UniswapV3 & Stakefish NTOKEN_BALANCE_EXCEEDED = "120", //ntoken balance exceed limit. - ORACLE_PRICE_NOT_READY = "121", //oracle price not ready - SET_ORACLE_SOURCE_NOT_ALLOWED = "122", //set oracle source not allowed - RESERVE_NOT_ACTIVE_FOR_UNIV3 = "123", //reserve is not active for UniswapV3. + ORACLE_PRICE_NOT_READY = "121", //oracle price not ready. + SET_ORACLE_SOURCE_NOT_ALLOWED = "122", //source of oracle not allowed to set. + INVALID_LIQUIDATION_ASSET = "123", //invalid liquidation asset. XTOKEN_TYPE_NOT_ALLOWED = "124", //the corresponding xTokenType not allowed in this action + GLOBAL_DEBT_IS_ZERO = "125", //liquidation is not allowed when global debt is zero. + ORACLE_PRICE_EXPIRED = "126", //oracle price expired. + APE_STAKING_POSITION_EXISTED = "127", //ape staking position is existed. SAPE_NOT_ALLOWED = "128", //operation is not allow for sApe. TOTAL_STAKING_AMOUNT_WRONG = "129", //cash plus borrow amount not equal to total staking amount. NOT_THE_BAKC_OWNER = "130", //user is not the bakc owner. + CALLER_NOT_EOA = "131", //The caller of the function is not an EOA account + MAKER_SAME_AS_TAKER = "132", //maker and taker shouldn't be the same address + TOKEN_ALREADY_DELEGATED = "133", //token is already delegted INVALID_STATE = "134", //invalid token status - INVALID_TOKEN_ID = "135", //invalid token id + SENDER_SAME_AS_RECEIVER = "136", //sender and receiver shouldn't be the same address + INVALID_YIELD_UNDERLYING_TOKEN = "137", //invalid yield underlying token + CALLER_NOT_OPERATOR = "138", // The caller of the function is not operator + INVALID_FEE_VALUE = "139", // invalid fee rate value + TOKEN_NOT_ALLOW_RESCUE = "140", // token is not allow rescue + CALL_SWAP_FAILED = "141", //call swap failed. + INVALID_SWAP_PAYLOAD = "142", //invalid swap payload. + SWAP_ADAPTER_PAUSED = "143", //swap adapter paused. // SafeCast SAFECAST_UINT128_OVERFLOW = "SafeCast: value doesn't fit in 128 bits", @@ -432,8 +449,6 @@ export enum ProtocolErrors { INVALID_HF = "Invalid health factor", //disable calls EMEGENCY_DISABLE_CALL = "emergency disable call", - - MAKER_SAME_AS_TAKER = "132", } export type tEthereumAddress = string; @@ -774,8 +789,14 @@ export interface IUniswapV3Config { NFTPositionManager: tEthereumAddress; } +export interface ISeaportConfig { + V11?: tEthereumAddress; + V14?: tEthereumAddress; + V15?: tEthereumAddress; +} + export interface IMarketplaceConfig { - Seaport?: tEthereumAddress; + Seaport?: ISeaportConfig; } export interface IChainlinkConfig { diff --git a/market-config/index.ts b/market-config/index.ts index 69bb652ac..b4df90ca2 100644 --- a/market-config/index.ts +++ b/market-config/index.ts @@ -319,7 +319,11 @@ export const GoerliConfig: IParaSpaceConfiguration = { V3NFTPositionManager: "0xC36442b4a4522E871399CD717aBDD847Ab11FE88", }, Marketplace: { - Seaport: "0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC", + Seaport: { + V11: "0x00000000006c3852cbEf3e08E8dF289169EdE581", + V14: "0x00000000000001ad428e4906aE43D8F9852d0dD6", + V15: "0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC", + }, }, BendDAO: { LendingPool: "0x84a47EaEca69f8B521C21739224251c8c4566Bbc", @@ -904,7 +908,11 @@ export const MainnetConfig: IParaSpaceConfiguration = { V3NFTPositionManager: "0xC36442b4a4522E871399CD717aBDD847Ab11FE88", }, Marketplace: { - Seaport: "0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC", + Seaport: { + V11: "0x00000000006c3852cbEf3e08E8dF289169EdE581", + V14: "0x00000000000001ad428e4906aE43D8F9852d0dD6", + V15: "0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC", + }, }, BendDAO: { LendingPool: "0x70b97a0da65c15dfb0ffa02aee6fa36e507c2762", diff --git a/scripts/deployments/steps/15_seaport.ts b/scripts/deployments/steps/15_seaport.ts index 6719f0567..cfb972605 100644 --- a/scripts/deployments/steps/15_seaport.ts +++ b/scripts/deployments/steps/15_seaport.ts @@ -11,7 +11,9 @@ import { getProtocolDataProvider, } from "../../../helpers/contracts-getters"; import { - OPENSEA_SEAPORT_ID, + OPENSEA_SEAPORT_ID_V11, + OPENSEA_SEAPORT_ID_V14, + OPENSEA_SEAPORT_ID_V15, PARASPACE_SEAPORT_ID, } from "../../../helpers/constants"; import {DRE, getParaSpaceConfig, waitForTx} from "../../../helpers/misc-utils"; @@ -83,13 +85,39 @@ export const step_15 = async (verify = false) => { ) ); - if (paraSpaceConfig.Marketplace.Seaport) { + if (paraSpaceConfig.Marketplace.Seaport?.V11) { await waitForTx( await addressesProvider.setMarketplace( - OPENSEA_SEAPORT_ID, - paraSpaceConfig.Marketplace.Seaport, + OPENSEA_SEAPORT_ID_V11, + paraSpaceConfig.Marketplace.Seaport.V11, seaportAdapter.address, - paraSpaceConfig.Marketplace.Seaport, + paraSpaceConfig.Marketplace.Seaport.V11, + false, + GLOBAL_OVERRIDES + ) + ); + } + + if (paraSpaceConfig.Marketplace.Seaport?.V14) { + await waitForTx( + await addressesProvider.setMarketplace( + OPENSEA_SEAPORT_ID_V14, + paraSpaceConfig.Marketplace.Seaport.V14, + seaportAdapter.address, + paraSpaceConfig.Marketplace.Seaport.V14, + false, + GLOBAL_OVERRIDES + ) + ); + } + + if (paraSpaceConfig.Marketplace.Seaport?.V15) { + await waitForTx( + await addressesProvider.setMarketplace( + OPENSEA_SEAPORT_ID_V15, + paraSpaceConfig.Marketplace.Seaport.V15, + seaportAdapter.address, + paraSpaceConfig.Marketplace.Seaport.V15, false, GLOBAL_OVERRIDES ) diff --git a/test/_pool_core_erc20_borrow_swap.spec.ts b/test/_pool_core_erc20_borrow_swap.spec.ts new file mode 100644 index 000000000..21cfe83b7 --- /dev/null +++ b/test/_pool_core_erc20_borrow_swap.spec.ts @@ -0,0 +1,141 @@ +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {expect} from "chai"; +import {solidityPack} from "ethers/lib/utils"; +import {UNISWAP_V3_SWAP_ADAPTER_ID} from "../helpers/constants"; +import {convertToCurrencyDecimals} from "../helpers/contracts-helpers"; +import {waitForTx} from "../helpers/misc-utils"; +import {testEnvFixture} from "./helpers/setup-env"; +import {supplyAndValidate} from "./helpers/validated-steps"; +import { + approveTo, + createNewPool, + fund, + mintNewPosition, +} from "./helpers/uniswapv3-helper"; +import {encodeSqrtRatioX96} from "@uniswap/v3-sdk"; +import {deployUniswapV3SwapAdapter} from "../helpers/contracts-deployments"; +import {getUniswapV3SwapRouter} from "../helpers/contracts-getters"; + +const fixture = async () => { + const testEnv = await loadFixture(testEnvFixture); + const { + usdc, + weth, + users: [user1, supplier], + nftPositionManager, + pool, + } = testEnv; + const swapAdapter = await deployUniswapV3SwapAdapter(false); + const swapRouter = await getUniswapV3SwapRouter(); + await waitForTx( + await pool.setSwapAdapter(UNISWAP_V3_SWAP_ADAPTER_ID, { + adapter: swapAdapter.address, + router: swapRouter.address, + paused: false, + }) + ); + + // User 1 - Deposit usdc + await supplyAndValidate(usdc, "20000", user1, true); + + // supplier + await supplyAndValidate(weth, "1000", supplier, true); + + //////////////////////////////////////////////////////////////////////////////// + // Uniswap USDC/WETH + //////////////////////////////////////////////////////////////////////////////// + const userUsdcAmount = await convertToCurrencyDecimals( + usdc.address, + "800000" + ); + const userWethAmount = await convertToCurrencyDecimals( + weth.address, + "732.76177" + ); + await fund({token: weth, user: supplier, amount: userWethAmount}); + await fund({token: usdc, user: supplier, amount: userUsdcAmount}); + const nft = nftPositionManager.connect(supplier.signer); + await approveTo({ + target: nftPositionManager.address, + token: usdc, + user: supplier, + }); + await approveTo({ + target: nftPositionManager.address, + token: weth, + user: supplier, + }); + const usdcWethFee = 500; + const usdcWethTickSpacing = usdcWethFee / 50; + const usdcWethInitialPrice = encodeSqrtRatioX96( + 1000000000000000000, + 1091760000 + ); + const usdcWethLowerPrice = encodeSqrtRatioX96(100000000000000000, 1091760000); + const usdcWethUpperPrice = encodeSqrtRatioX96( + 10000000000000000000, + 1091760000 + ); + await createNewPool({ + positionManager: nft, + token0: usdc, + token1: weth, + fee: usdcWethFee, + initialSqrtPrice: usdcWethInitialPrice.toString(), + }); + await mintNewPosition({ + nft: nft, + token0: usdc, + token1: weth, + fee: usdcWethFee, + user: supplier, + tickSpacing: usdcWethTickSpacing, + lowerPrice: usdcWethLowerPrice, + upperPrice: usdcWethUpperPrice, + token0Amount: userUsdcAmount, + token1Amount: userWethAmount, + }); + + return testEnv; +}; + +describe("Borrow ERC20 and swap", () => { + it("TC-erc20-borrow-01: user1 borrow usdc and swap to weth", async () => { + const { + pool, + users: [user1], + usdc, + pUsdc, + weth, + } = await loadFixture(fixture); + const swapRouter = await getUniswapV3SwapRouter(); + const borrowAmount = await convertToCurrencyDecimals(usdc.address, "1000"); + const swapPayload = swapRouter.interface.encodeFunctionData("exactInput", [ + { + path: solidityPack( + ["address", "uint24", "address"], + [usdc.address, 500, weth.address] + ), + recipient: pUsdc.address, + deadline: 2659537628, + amountIn: borrowAmount, + amountOutMinimum: 0, + }, + ]); + + await waitForTx( + await pool + .connect(user1.signer) + .borrowAny( + usdc.address, + borrowAmount, + 0, + user1.address, + UNISWAP_V3_SWAP_ADAPTER_ID, + `0x${swapPayload.slice(10)}` + ) + ); + + expect(await weth.balanceOf(user1.address)).gt(0); + }); +}); diff --git a/test/_pool_core_erc20_debt_swap.spec.ts b/test/_pool_core_erc20_debt_swap.spec.ts new file mode 100644 index 000000000..eed08a204 --- /dev/null +++ b/test/_pool_core_erc20_debt_swap.spec.ts @@ -0,0 +1,221 @@ +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {expect} from "chai"; +import {solidityPack} from "ethers/lib/utils"; +import { + MAX_UINT_AMOUNT, + UNISWAP_V3_SWAP_ADAPTER_ID, +} from "../helpers/constants"; +import { + convertToCurrencyDecimals, + isBorrowing, +} from "../helpers/contracts-helpers"; +import {waitForTx} from "../helpers/misc-utils"; +import {testEnvFixture} from "./helpers/setup-env"; +import {supplyAndValidate} from "./helpers/validated-steps"; +import { + almostEqual, + approveTo, + createNewPool, + fund, + mintNewPosition, +} from "./helpers/uniswapv3-helper"; +import {encodeSqrtRatioX96} from "@uniswap/v3-sdk"; +import {deployUniswapV3SwapAdapter} from "../helpers/contracts-deployments"; +import {getUniswapV3SwapRouter} from "../helpers/contracts-getters"; +import {ProtocolErrors} from "../helpers/types"; + +const fixture = async () => { + const testEnv = await loadFixture(testEnvFixture); + const { + usdc, + weth, + users: [user1, supplier], + nftPositionManager, + pool, + } = testEnv; + const swapAdapter = await deployUniswapV3SwapAdapter(false); + const swapRouter = await getUniswapV3SwapRouter(); + await waitForTx( + await pool.setSwapAdapter(UNISWAP_V3_SWAP_ADAPTER_ID, { + adapter: swapAdapter.address, + router: swapRouter.address, + paused: false, + }) + ); + + // User 1 - Deposit usdc + await supplyAndValidate(usdc, "20000", user1, true); + + // supplier + await supplyAndValidate(weth, "1000", supplier, true); + + //////////////////////////////////////////////////////////////////////////////// + // Uniswap USDC/WETH + //////////////////////////////////////////////////////////////////////////////// + const userUsdcAmount = await convertToCurrencyDecimals( + usdc.address, + "800000" + ); + const userWethAmount = await convertToCurrencyDecimals( + weth.address, + "732.76177" + ); + await fund({token: weth, user: supplier, amount: userWethAmount}); + await fund({token: usdc, user: supplier, amount: userUsdcAmount}); + const nft = nftPositionManager.connect(supplier.signer); + await approveTo({ + target: nftPositionManager.address, + token: usdc, + user: supplier, + }); + await approveTo({ + target: nftPositionManager.address, + token: weth, + user: supplier, + }); + const usdcWethFee = 500; + const usdcWethTickSpacing = usdcWethFee / 50; + const usdcWethInitialPrice = encodeSqrtRatioX96( + 1000000000000000000, + 1091760000 + ); + const usdcWethLowerPrice = encodeSqrtRatioX96(100000000000000000, 1091760000); + const usdcWethUpperPrice = encodeSqrtRatioX96( + 10000000000000000000, + 1091760000 + ); + await createNewPool({ + positionManager: nft, + token0: usdc, + token1: weth, + fee: usdcWethFee, + initialSqrtPrice: usdcWethInitialPrice.toString(), + }); + await mintNewPosition({ + nft: nft, + token0: usdc, + token1: weth, + fee: usdcWethFee, + user: supplier, + tickSpacing: usdcWethTickSpacing, + lowerPrice: usdcWethLowerPrice, + upperPrice: usdcWethUpperPrice, + token0Amount: userUsdcAmount, + token1Amount: userWethAmount, + }); + + return testEnv; +}; + +describe("Debt swap", () => { + it("TC-erc20-debt-swap-01: user1 borrow weth and swap debt to usdc", async () => { + const { + pool, + users: [user1], + usdc, + variableDebtUsdc, + variableDebtWeth, + weth, + pUsdc, + } = await loadFixture(fixture); + + const swapRouter = await getUniswapV3SwapRouter(); + const borrowAmount = await convertToCurrencyDecimals(weth.address, "1"); + const swapPayload = swapRouter.interface.encodeFunctionData("exactOutput", [ + { + path: solidityPack( + ["address", "uint24", "address"], + [weth.address, 500, usdc.address] + ), + recipient: pUsdc.address, + deadline: 2659537628, + amountOut: borrowAmount, + amountInMaximum: MAX_UINT_AMOUNT, + }, + ]); + + await waitForTx( + await pool + .connect(user1.signer) + .borrow(weth.address, borrowAmount, 0, user1.address) + ); + + const beforeUsdcDebt = await variableDebtUsdc.balanceOf(user1.address); + const beforeWethDebt = await variableDebtWeth.balanceOf(user1.address); + + await waitForTx( + await pool + .connect(user1.signer) + .swapDebt( + weth.address, + borrowAmount, + usdc.address, + UNISWAP_V3_SWAP_ADAPTER_ID, + `0x${swapPayload.slice(10)}` + ) + ); + + const afterConfigData = await pool.getUserConfiguration(user1.address); + almostEqual( + beforeWethDebt.sub(await variableDebtWeth.balanceOf(user1.address)), + borrowAmount + ); + expect( + isBorrowing( + afterConfigData.data, + (await pool.getReserveData(usdc.address)).id + ) + ); + + almostEqual( + (await variableDebtUsdc.balanceOf(user1.address)).sub(beforeUsdcDebt), + await convertToCurrencyDecimals(usdc.address, "1093") + ); + }); + + it("TC-erc20-debt-swap-02: user1 borrow maximum of eth and swap debt to usdc (reverted expected)", async () => { + const { + pool, + users: [user1], + usdc, + weth, + pUsdc, + } = await loadFixture(fixture); + + const swapRouter = await getUniswapV3SwapRouter(); + const borrowAmount = await convertToCurrencyDecimals( + weth.address, + "15.937568696" + ); + const swapPayload = swapRouter.interface.encodeFunctionData("exactOutput", [ + { + path: solidityPack( + ["address", "uint24", "address"], + [weth.address, 500, usdc.address] + ), + recipient: pUsdc.address, + deadline: 2659537628, + amountOut: borrowAmount, + amountInMaximum: MAX_UINT_AMOUNT, + }, + ]); + + await waitForTx( + await pool + .connect(user1.signer) + .borrow(weth.address, borrowAmount, 0, user1.address) + ); + + await expect( + pool + .connect(user1.signer) + .swapDebt( + weth.address, + borrowAmount, + usdc.address, + UNISWAP_V3_SWAP_ADAPTER_ID, + `0x${swapPayload.slice(10)}` + ) + ).to.be.revertedWith(ProtocolErrors.COLLATERAL_CANNOT_COVER_NEW_BORROW); + }); +}); diff --git a/test/_pool_core_erc20_ptoken_swap.spec.ts b/test/_pool_core_erc20_ptoken_swap.spec.ts new file mode 100644 index 000000000..6e90a8d92 --- /dev/null +++ b/test/_pool_core_erc20_ptoken_swap.spec.ts @@ -0,0 +1,310 @@ +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {expect} from "chai"; +import {parseEther, solidityPack} from "ethers/lib/utils"; +import {UNISWAP_V3_SWAP_ADAPTER_ID} from "../helpers/constants"; +import { + convertToCurrencyDecimals, + isUsingAsCollateral, +} from "../helpers/contracts-helpers"; +import {waitForTx} from "../helpers/misc-utils"; +import {testEnvFixture} from "./helpers/setup-env"; +import {supplyAndValidate} from "./helpers/validated-steps"; +import { + almostEqual, + approveTo, + createNewPool, + fund, + mintNewPosition, +} from "./helpers/uniswapv3-helper"; +import {encodeSqrtRatioX96} from "@uniswap/v3-sdk"; +import {deployUniswapV3SwapAdapter} from "../helpers/contracts-deployments"; +import {getUniswapV3SwapRouter} from "../helpers/contracts-getters"; +import {ProtocolErrors} from "../helpers/types"; + +const fixture = async () => { + const testEnv = await loadFixture(testEnvFixture); + const { + usdc, + weth, + users: [user1, supplier], + nftPositionManager, + pool, + } = testEnv; + const swapAdapter = await deployUniswapV3SwapAdapter(false); + const swapRouter = await getUniswapV3SwapRouter(); + await waitForTx( + await pool.setSwapAdapter(UNISWAP_V3_SWAP_ADAPTER_ID, { + adapter: swapAdapter.address, + router: swapRouter.address, + paused: false, + }) + ); + + // User 1 - Deposit usdc + await supplyAndValidate(usdc, "20000", user1, true); + + // supplier + await supplyAndValidate(weth, "1000", supplier, true); + + //////////////////////////////////////////////////////////////////////////////// + // Uniswap USDC/WETH + //////////////////////////////////////////////////////////////////////////////// + const userUsdcAmount = await convertToCurrencyDecimals( + usdc.address, + "800000" + ); + const userWethAmount = await convertToCurrencyDecimals( + weth.address, + "732.76177" + ); + await fund({token: weth, user: supplier, amount: userWethAmount}); + await fund({token: usdc, user: supplier, amount: userUsdcAmount}); + const nft = nftPositionManager.connect(supplier.signer); + await approveTo({ + target: nftPositionManager.address, + token: usdc, + user: supplier, + }); + await approveTo({ + target: nftPositionManager.address, + token: weth, + user: supplier, + }); + const usdcWethFee = 500; + const usdcWethTickSpacing = usdcWethFee / 50; + const usdcWethInitialPrice = encodeSqrtRatioX96( + 1000000000000000000, + 1091760000 + ); + const usdcWethLowerPrice = encodeSqrtRatioX96(100000000000000000, 1091760000); + const usdcWethUpperPrice = encodeSqrtRatioX96( + 10000000000000000000, + 1091760000 + ); + await createNewPool({ + positionManager: nft, + token0: usdc, + token1: weth, + fee: usdcWethFee, + initialSqrtPrice: usdcWethInitialPrice.toString(), + }); + await mintNewPosition({ + nft: nft, + token0: usdc, + token1: weth, + fee: usdcWethFee, + user: supplier, + tickSpacing: usdcWethTickSpacing, + lowerPrice: usdcWethLowerPrice, + upperPrice: usdcWethUpperPrice, + token0Amount: userUsdcAmount, + token1Amount: userWethAmount, + }); + + return testEnv; +}; + +describe("PToken swap", () => { + it("TC-erc20-ptoken-swap-01: user1 supply usdc and swap pUSDC to pWeth", async () => { + const { + pool, + users: [user1], + usdc, + pUsdc, + pWETH, + weth, + } = await loadFixture(fixture); + const swapRouter = await getUniswapV3SwapRouter(); + const swapAmount = await convertToCurrencyDecimals(usdc.address, "1000"); + const beforePusdc = await pUsdc.balanceOf(user1.address); + const beforePweth = await pWETH.balanceOf(user1.address); + const swapPayload = swapRouter.interface.encodeFunctionData("exactInput", [ + { + path: solidityPack( + ["address", "uint24", "address"], + [usdc.address, 500, weth.address] + ), + recipient: pUsdc.address, + deadline: 2659537628, + amountIn: swapAmount, + amountOutMinimum: 0, + }, + ]); + + await waitForTx( + await pool + .connect(user1.signer) + .swapPToken( + usdc.address, + swapAmount, + weth.address, + user1.address, + UNISWAP_V3_SWAP_ADAPTER_ID, + `0x${swapPayload.slice(10)}` + ) + ); + + const afterConfigData = await pool.getUserConfiguration(user1.address); + + expect(await pWETH.balanceOf(user1.address)).gt(0); + expect(beforePusdc.sub(await pUsdc.balanceOf(user1.address))).eq( + swapAmount + ); + almostEqual( + (await pWETH.balanceOf(user1.address)).sub(beforePweth), + parseEther("0.914712") + ); + expect( + isUsingAsCollateral( + afterConfigData.data, + (await pool.getReserveData(weth.address)).id + ) + ); + }); + + it("TC-erc20-ptoken-swap-02: user1 supply usdc, borrow maximum and swap pUSDC to pWeth", async () => { + const { + pool, + users: [user1], + usdc, + pUsdc, + weth, + } = await loadFixture(fixture); + const swapRouter = await getUniswapV3SwapRouter(); + const swapAmount = await convertToCurrencyDecimals(usdc.address, "20000"); + const swapPayload = swapRouter.interface.encodeFunctionData("exactInput", [ + { + path: solidityPack( + ["address", "uint24", "address"], + [usdc.address, 500, weth.address] + ), + recipient: pUsdc.address, + deadline: 2659537628, + amountIn: swapAmount, + amountOutMinimum: 0, + }, + ]); + + await waitForTx( + await pool + .connect(user1.signer) + .borrow(weth.address, parseEther("15.937568696"), 0, user1.address) + ); + + await expect( + pool + .connect(user1.signer) + .swapPToken( + usdc.address, + swapAmount, + weth.address, + user1.address, + UNISWAP_V3_SWAP_ADAPTER_ID, + `0x${swapPayload.slice(10)}` + ) + ).to.be.revertedWith( + ProtocolErrors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD + ); + }); + + it("TC-erc20-ptoken-swap-03: user1 supply usdc, borrow half and swap half of pUSDC to pWETH and send to another user (revert expected)", async () => { + const { + pool, + users: [user1, , user3], + usdc, + pUsdc, + weth, + } = await loadFixture(fixture); + const swapRouter = await getUniswapV3SwapRouter(); + const swapAmount = await convertToCurrencyDecimals(usdc.address, "10000"); + const swapPayload = swapRouter.interface.encodeFunctionData("exactInput", [ + { + path: solidityPack( + ["address", "uint24", "address"], + [usdc.address, 500, weth.address] + ), + recipient: pUsdc.address, + deadline: 2659537628, + amountIn: swapAmount, + amountOutMinimum: 0, + }, + ]); + + await waitForTx( + await pool + .connect(user1.signer) + .borrow(weth.address, parseEther("8.5"), 0, user1.address) + ); + + await expect( + pool + .connect(user1.signer) + .swapPToken( + usdc.address, + swapAmount, + weth.address, + user3.address, + UNISWAP_V3_SWAP_ADAPTER_ID, + `0x${swapPayload.slice(10)}` + ) + ).to.be.revertedWith( + ProtocolErrors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD + ); + }); + + it("TC-erc20-ptoken-swap-04: user1 supply usdc and swap pUSDC to pWeth then send to another user", async () => { + const { + pool, + users: [user1, , user3], + usdc, + pUsdc, + pWETH, + weth, + } = await loadFixture(fixture); + const swapRouter = await getUniswapV3SwapRouter(); + const swapAmount = await convertToCurrencyDecimals(usdc.address, "1000"); + const beforePusdc = await pUsdc.balanceOf(user1.address); + const beforePweth = await pWETH.balanceOf(user3.address); + const swapPayload = swapRouter.interface.encodeFunctionData("exactInput", [ + { + path: solidityPack( + ["address", "uint24", "address"], + [usdc.address, 500, weth.address] + ), + recipient: pUsdc.address, + deadline: 2659537628, + amountIn: swapAmount, + amountOutMinimum: 0, + }, + ]); + + await waitForTx( + await pool + .connect(user1.signer) + .swapPToken( + usdc.address, + swapAmount, + weth.address, + user3.address, + UNISWAP_V3_SWAP_ADAPTER_ID, + `0x${swapPayload.slice(10)}` + ) + ); + + const configData = await pool.getUserConfiguration(user3.address); + + expect(beforePusdc.sub(await pUsdc.balanceOf(user1.address))).eq( + swapAmount + ); + almostEqual( + (await pWETH.balanceOf(user3.address)).sub(beforePweth), + parseEther("0.914712") + ); + expect( + isUsingAsCollateral( + configData.data, + (await pool.getReserveData(weth.address)).id + ) + ); + }); +}); diff --git a/test/_pool_marketplace_accept_bid_with_credit.spec.ts b/test/_pool_marketplace_accept_bid_with_credit.spec.ts index 5589ed005..02c7ff3f8 100644 --- a/test/_pool_marketplace_accept_bid_with_credit.spec.ts +++ b/test/_pool_marketplace_accept_bid_with_credit.spec.ts @@ -19,7 +19,7 @@ import { toBN, toFulfillment, } from "../helpers/seaport-helpers/encoding"; -import {MAX_UINT_AMOUNT, PARASPACE_SEAPORT_ID} from "../helpers/constants"; +import {PARASPACE_SEAPORT_ID, ZERO_ADDRESS} from "../helpers/constants"; import {arrayify, splitSignature} from "ethers/lib/utils"; import {BigNumber} from "ethers"; import { @@ -613,7 +613,6 @@ describe("Leveraged Bid - unit tests", () => { }, ], taker.address, - 0, { gasLimit: 5000000, } @@ -798,7 +797,6 @@ describe("Leveraged Bid - unit tests", () => { ...vrs, }, taker.address, - 0, { gasLimit: 5000000, } @@ -983,7 +981,6 @@ describe("Leveraged Bid - unit tests", () => { ...vrs, }, taker.address, - 0, { gasLimit: 5000000, } @@ -1197,7 +1194,6 @@ describe("Leveraged Bid - unit tests", () => { ...vrs, }, taker.address, - 0, { gasLimit: 5000000, } @@ -1274,27 +1270,15 @@ describe("Leveraged Bid - unit tests", () => { const nftId = 0; // mint USDC to maker - await mintAndValidate(usdc, makerInitialBalance, maker); + await supplyAndValidate(usdc, makerInitialBalance, maker, true); // middleman supplies USDC to pool to be borrowed by maker later await supplyAndValidate(usdc, middlemanInitialBalance, middleman, true); await supplyAndValidate(usdc, makerInitialDebt, middleman, true); - expect( - await usdc.balanceOf( - ( - await pool.getReserveData(usdc.address) - ).xTokenAddress - ) - ).to.be.equal(creditAmount.add(borrowAmount)); - await supplyAndValidate(bayc, "1", taker, true); await borrowAndValidate(usdc, makerInitialDebt, taker); - await waitForTx( - await usdc.connect(taker.signer).approve(pool.address, MAX_UINT_AMOUNT) - ); - // before acceptBidWithCredit totalCollateralBase for the taker // is just the bayc const totalCollateralBaseBefore = ( @@ -1309,13 +1293,14 @@ describe("Leveraged Bid - unit tests", () => { await executeAcceptBidWithCredit( nBAYC, - usdc, + pUsdc, startAmount, endAmount, creditAmount, nftId, maker, - taker + taker, + true ); const usdcConfigData = BigNumber.from( (await pool.getUserConfiguration(taker.address)).data @@ -1325,12 +1310,8 @@ describe("Leveraged Bid - unit tests", () => { // taker bayc should reduce expect(await nBAYC.balanceOf(taker.address)).to.be.equal(0); expect(await nBAYC.ownerOf(nftId)).to.be.equal(maker.address); - expect(await usdc.balanceOf(taker.address)).to.be.equal( - startAmount.percentMul("500").add(borrowAmount) - ); - expect(await pUsdc.balanceOf(taker.address)).to.be.equal( - startAmount.percentMul("9500") - ); + expect(await usdc.balanceOf(taker.address)).to.be.equal(borrowAmount); + expect(await pUsdc.balanceOf(taker.address)).to.be.equal(startAmount); expect(isUsingAsCollateral(usdcConfigData, usdcReserveData.id)).to.be.true; // after the swap offer's totalCollateralBase should be same as taker's before @@ -1345,6 +1326,271 @@ describe("Leveraged Bid - unit tests", () => { totalDebtBefore.add(accrualTotalDebtBase) ); }); + + it("TC-erc721-bid-10 ERC20 <=> NToken, accept without credit borrow. NToken converted to underlying asset.", async () => { + const { + bayc, + nBAYC, + usdc, + pool, + seaport, + pausableZone, + conduit, + conduitKey, + users: [maker, taker], + } = await loadFixture(testEnvFixture); + const makerInitialBalance = "1000"; + const payNowAmount = await convertToCurrencyDecimals(usdc.address, "1000"); + const creditAmount = await convertToCurrencyDecimals(usdc.address, "0"); + const startAmount = payNowAmount.add(creditAmount); + const endAmount = startAmount; // fixed price but offerer cannot afford this + const nftId = 0; + + // mint USDC to maker + await mintAndValidate(usdc, makerInitialBalance, maker); + // mint BAYC to taker + await supplyAndValidate(bayc, "1", taker, true); + + await waitForTx( + await usdc.connect(maker.signer).approve(conduit.address, startAmount) + ); + await waitForTx( + await bayc.connect(taker.signer).setApprovalForAll(conduit.address, true) + ); + + const getSellOrder = async (): Promise => { + const offers = [ + getOfferOrConsiderationItem( + 1, + usdc.address, + toBN(0), + startAmount, + endAmount + ), + ]; + + const considerations = [ + getOfferOrConsiderationItem( + 2, + bayc.address, + nftId, + toBN(1), + toBN(1), + maker.address + ), + ]; + + return createSeaportOrder( + seaport, + maker, + offers, + considerations, + 2, + pausableZone.address, + conduitKey + ); + }; + + const getBuyOrder = async (): Promise => { + const offers = [ + getOfferOrConsiderationItem(2, bayc.address, nftId, toBN(1), toBN(1)), + ]; + + const considerations = [ + getOfferOrConsiderationItem( + 1, + usdc.address, + toBN(0), + startAmount, + endAmount, + taker.address + ), + ]; + + return createSeaportOrder( + seaport, + taker, + offers, + considerations, + 2, + pausableZone.address, + conduitKey + ); + }; + + const fulfillment = [ + [[[0, 0]], [[1, 0]]], + [[[1, 0]], [[0, 0]]], + ].map(([makerArr, considerationArr]) => + toFulfillment(makerArr, considerationArr) + ); + + const sellOrder = await getSellOrder(); + const buyOrder = await getBuyOrder(); + + const encodedData = seaport.interface.encodeFunctionData( + "matchAdvancedOrders", + [[sellOrder, buyOrder], [], fulfillment] + ); + + const tx = pool + .connect(taker.signer) + .acceptOpenseaBid( + PARASPACE_SEAPORT_ID, + `0x${encodedData.slice(10)}`, + taker.address, + { + gasLimit: 5000000, + } + ); + + await (await tx).wait(); + + expect(await bayc.ownerOf(nftId)).eq(maker.address); + expect(await nBAYC.ownerOf(nftId)).eq(ZERO_ADDRESS); + expect(await usdc.balanceOf(taker.address)).eq(startAmount); + }); + + it("TC-erc721-bid-11 ERC20 <=> NToken Bundle, accept without credit borrow. NToken converted to underlying asset.", async () => { + const { + bayc, + nBAYC, + usdc, + pool, + seaport, + pausableZone, + conduit, + conduitKey, + users: [maker, taker], + } = await loadFixture(testEnvFixture); + const makerInitialBalance = "1000"; + const payNowAmount = await convertToCurrencyDecimals(usdc.address, "1000"); + const creditAmount = await convertToCurrencyDecimals(usdc.address, "0"); + const startAmount = payNowAmount.add(creditAmount); + const endAmount = startAmount; // fixed price but offerer cannot afford this + + // mint USDC to maker + await mintAndValidate(usdc, makerInitialBalance, maker); + // mint BAYC to taker + await supplyAndValidate(bayc, "2", taker, true); + + await waitForTx( + await pool + .connect(taker.signer) + .withdrawERC721(bayc.address, ["1"], taker.address) + ); + + await waitForTx( + await usdc.connect(maker.signer).approve(conduit.address, startAmount) + ); + await waitForTx( + await bayc.connect(taker.signer).setApprovalForAll(conduit.address, true) + ); + + const getSellOrder = async (): Promise => { + const offers = [ + getOfferOrConsiderationItem( + 1, + usdc.address, + toBN(0), + startAmount, + endAmount + ), + ]; + + const considerations = [ + getOfferOrConsiderationItem( + 2, + bayc.address, + "0", + toBN(1), + toBN(1), + maker.address + ), + getOfferOrConsiderationItem( + 2, + bayc.address, + "1", + toBN(1), + toBN(1), + maker.address + ), + ]; + + return createSeaportOrder( + seaport, + maker, + offers, + considerations, + 2, + pausableZone.address, + conduitKey + ); + }; + + const getBuyOrder = async (): Promise => { + const offers = [ + getOfferOrConsiderationItem(2, bayc.address, "0", toBN(1), toBN(1)), + getOfferOrConsiderationItem(2, bayc.address, "1", toBN(1), toBN(1)), + ]; + + const considerations = [ + getOfferOrConsiderationItem( + 1, + usdc.address, + toBN(0), + startAmount, + endAmount, + taker.address + ), + ]; + + return createSeaportOrder( + seaport, + taker, + offers, + considerations, + 2, + pausableZone.address, + conduitKey + ); + }; + + const fulfillment = [ + [[[0, 0]], [[1, 0]]], + [[[1, 0]], [[0, 0]]], + [[[1, 1]], [[0, 1]]], + ].map(([makerArr, considerationArr]) => + toFulfillment(makerArr, considerationArr) + ); + + const sellOrder = await getSellOrder(); + const buyOrder = await getBuyOrder(); + + const encodedData = seaport.interface.encodeFunctionData( + "matchAdvancedOrders", + [[sellOrder, buyOrder], [], fulfillment] + ); + + const tx = pool + .connect(taker.signer) + .acceptOpenseaBid( + PARASPACE_SEAPORT_ID, + `0x${encodedData.slice(10)}`, + taker.address, + { + gasLimit: 5000000, + } + ); + + await (await tx).wait(); + + expect(await bayc.ownerOf(0)).eq(maker.address); + expect(await bayc.ownerOf(1)).eq(maker.address); + expect(await nBAYC.ownerOf(0)).eq(ZERO_ADDRESS); + expect(await nBAYC.ownerOf(1)).eq(ZERO_ADDRESS); + expect(await usdc.balanceOf(taker.address)).eq(startAmount); + }); }); describe("Leveraged Bid - Negative tests", () => { @@ -1566,7 +1812,7 @@ describe("Leveraged Bid - Negative tests", () => { maker, taker ) - ).to.be.revertedWith(ProtocolErrors.ASSET_NOT_LISTED); + ).to.be.reverted; }); it("TC-erc721-bid-14 cannot credit amount above the NFT's LTV (should fail)", async () => { @@ -1819,7 +2065,6 @@ describe("Leveraged Bid - Negative tests", () => { ...vrs, }, [nftId], - 0, { gasLimit: 5000000, } diff --git a/test/_pool_marketplace_adapter.spec.ts b/test/_pool_marketplace_adapter.spec.ts index 63b1538f9..840f386da 100644 --- a/test/_pool_marketplace_adapter.spec.ts +++ b/test/_pool_marketplace_adapter.spec.ts @@ -15,10 +15,6 @@ import { SignatureVersion, } from "../helpers/blur-helpers/types"; import {ZERO_ADDRESS} from "../helpers/constants"; -import { - getStandardPolicyERC721, - getStrategyStandardSaleForFixedPrice, -} from "../helpers/contracts-getters"; import { createBlurOrder, createSeaportOrder, @@ -44,49 +40,6 @@ describe("Marketplace Adapters - Negative Tests", () => { const nftId = "0"; const nftPrice = parseEther("50"); - it("TC-seaportAdapter-01: getAskOrderInfo will fail if fulfillAdvancedOrder dont specify pool as ERC721 recipient (revert expected)", async () => { - const { - seaportAdapter, - bayc, - usdt, - users: [maker, taker], - seaport, - pausableZone, - conduitKey, - } = await loadFixture(testEnvFixture); - const offers = [ - getOfferOrConsiderationItem(2, bayc.address, nftId, toBN(1), toBN(1)), - ]; - const considerations = [ - getOfferOrConsiderationItem( - 1, - usdt.address, - toBN(0), - nftPrice, - nftPrice, - maker.address - ), - ]; - const listing: AdvancedOrder = await createSeaportOrder( - seaport, - maker, - offers, - considerations, - 2, - pausableZone.address, - conduitKey - ); - - const encodedData = seaport.interface.encodeFunctionData( - "fulfillAdvancedOrder", - [listing, [], conduitKey, taker.address] - ); - - await expect( - seaportAdapter.getAskOrderInfo(`0x${encodedData.slice(10)}`) - ).to.be.revertedWith(ProtocolErrors.INVALID_ORDER_TAKER); - }); - it("TC-seaportAdapter-02: getBidOrderInfo will fail if maker & taker are the same address (revert expected)", async () => { const { seaportAdapter, @@ -175,78 +128,6 @@ describe("Marketplace Adapters - Negative Tests", () => { ).to.be.revertedWith(ProtocolErrors.MAKER_SAME_AS_TAKER); }); - it("TC-looksRareAdapter-01: getAskOrderInfo will fail if taker isn't pool (revert expected)", async () => { - const { - looksRareAdapter, - looksRareExchange, - users: [maker], - bayc, - usdt, - } = await loadFixture(testEnvFixture); - - const now = Math.floor(Date.now() / 1000); - const chainId = await maker.signer.getChainId(); - const paramsValue = []; - const makerOrder: MakerOrder = { - isOrderAsk: true, - signer: maker.address, - collection: bayc.address, - price: nftPrice, - tokenId: nftId, - amount: "1", - strategy: (await getStrategyStandardSaleForFixedPrice()).address, - currency: usdt.address, - nonce: await maker.signer.getTransactionCount(), - startTime: now - 86400, - endTime: now + 86400, // 2 days validity - minPercentageToAsk: 7500, - params: paramsValue, - }; - - const {domain, value, type} = generateMakerOrderTypedData( - maker.address, - chainId, - makerOrder, - looksRareExchange.address - ); - - const signatureHash = await DRE.ethers.provider - .getSigner(maker.address) - ._signTypedData(domain, type, value); - - const makerOrderWithSignature: MakerOrderWithSignature = { - ...makerOrder, - signature: signatureHash, - }; - - const vrs = DRE.ethers.utils.splitSignature( - makerOrderWithSignature.signature - ); - - const makerOrderWithVRS: MakerOrderWithVRS = { - ...makerOrderWithSignature, - ...vrs, - }; - - const takerOrder: TakerOrder = { - isOrderAsk: false, - taker: maker.address, - price: makerOrderWithSignature.price, - tokenId: makerOrderWithSignature.tokenId, - minPercentageToAsk: 7500, - params: paramsValue, - }; - - const encodedData = looksRareExchange.interface.encodeFunctionData( - "matchAskWithTakerBid", - [takerOrder, makerOrderWithVRS] - ); - - await expect( - looksRareAdapter.getAskOrderInfo(`0x${encodedData.slice(10)}`) - ).to.be.revertedWith(ProtocolErrors.INVALID_ORDER_TAKER); - }); - it("TC-looksRareAdapter-02: getAskOrderInfo will fail if matching strategy isn't StandardSaleForFixedPrice (revert expected)", async () => { const { looksRareAdapter, @@ -461,66 +342,4 @@ describe("Marketplace Adapters - Negative Tests", () => { blurAdapter.getAskOrderInfo(`0x${encodedData.slice(10)}`) ).to.be.revertedWith(ProtocolErrors.INVALID_MARKETPLACE_ORDER); }); - - it("TC-blurAdapter-02: getAskOrderInfo will fail if taker isn't pool (revert expected)", async () => { - const { - blurAdapter, - blurExchange, - users: [maker], - bayc, - usdt, - } = await loadFixture(testEnvFixture); - - const now = Math.floor(Date.now() / 1000); - const makerOrder: BlurOrder = { - trader: maker.address, - side: Side.Sell, - matchingPolicy: (await getStandardPolicyERC721()).address, - collection: bayc.address, - tokenId: nftId, - amount: "1", - paymentToken: usdt.address, - price: nftPrice, - listingTime: now - 86400, - expirationTime: now + 86400, - fees: [], - salt: randomHex(), - extraParams: "0x", - }; - const takerOrder: BlurOrder = { - trader: maker.address, - side: Side.Buy, - matchingPolicy: (await getStandardPolicyERC721()).address, - collection: bayc.address, - tokenId: nftId, - amount: "1", - paymentToken: usdt.address, - price: nftPrice, - listingTime: now - 86400, - expirationTime: now + 86400, - fees: [], - salt: randomHex(), - extraParams: "0x", - }; - - const makerInput = await createBlurOrder(blurExchange, maker, makerOrder); - const takerInput = { - order: takerOrder, - v: 0, - r: constants.HashZero, - s: constants.HashZero, - extraSignature: "0x", - signatureVersion: SignatureVersion.Single, - blockNumber: 0, - }; - - const encodedData = blurExchange.interface.encodeFunctionData("execute", [ - makerInput as InputStruct, - takerInput as InputStruct, - ]); - - await expect( - blurAdapter.getAskOrderInfo(`0x${encodedData.slice(10)}`) - ).to.be.revertedWith(ProtocolErrors.INVALID_ORDER_TAKER); - }); }); diff --git a/test/_pool_marketplace_buy_any_with_credit.spec.ts b/test/_pool_marketplace_buy_any_with_credit.spec.ts new file mode 100644 index 000000000..8b170c276 --- /dev/null +++ b/test/_pool_marketplace_buy_any_with_credit.spec.ts @@ -0,0 +1,289 @@ +import {expect} from "chai"; +import {waitForTx} from "../helpers/misc-utils"; +import { + convertToCurrencyDecimals, + createSeaportOrder, +} from "../helpers/contracts-helpers"; +import {AdvancedOrder} from "../helpers/seaport-helpers/types"; +import { + getOfferOrConsiderationItem, + toBN, + toFulfillment, +} from "../helpers/seaport-helpers/encoding"; +import { + MAX_UINT_AMOUNT, + PARASPACE_SEAPORT_ID, + UNISWAP_V3_SWAP_ADAPTER_ID, +} from "../helpers/constants"; +import {mintAndValidate, supplyAndValidate} from "./helpers/validated-steps"; +import {getUniswapV3SwapRouter} from "../helpers/contracts-getters"; +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {testEnvFixture} from "./helpers/setup-env"; +import {deployUniswapV3SwapAdapter} from "../helpers/contracts-deployments"; +import { + approveTo, + createNewPool, + mintNewPosition, + fund, +} from "./helpers/uniswapv3-helper"; +import {encodeSqrtRatioX96} from "@uniswap/v3-sdk"; +import {constants} from "ethers"; +import {solidityPack} from "ethers/lib/utils"; + +describe("Leveraged Buy Any - Positive tests", () => { + const fixture = async () => { + const testEnv = await loadFixture(testEnvFixture); + const { + usdc, + weth, + users: [, , supplier], + nftPositionManager, + pool, + } = testEnv; + const swapAdapter = await deployUniswapV3SwapAdapter(false); + const swapRouter = await getUniswapV3SwapRouter(); + await waitForTx( + await pool.setSwapAdapter(UNISWAP_V3_SWAP_ADAPTER_ID, { + adapter: swapAdapter.address, + router: swapRouter.address, + paused: false, + }) + ); + + await supplyAndValidate(usdc, "20000", supplier, true); + await supplyAndValidate(weth, "1000", supplier, true); + + //////////////////////////////////////////////////////////////////////////////// + // Uniswap USDC/WETH + //////////////////////////////////////////////////////////////////////////////// + const userUsdcAmount = await convertToCurrencyDecimals( + usdc.address, + "800000" + ); + const userWethAmount = await convertToCurrencyDecimals( + weth.address, + "732.76177" + ); + await fund({token: weth, user: supplier, amount: userWethAmount}); + await fund({token: usdc, user: supplier, amount: userUsdcAmount}); + const nft = nftPositionManager.connect(supplier.signer); + await approveTo({ + target: nftPositionManager.address, + token: usdc, + user: supplier, + }); + await approveTo({ + target: nftPositionManager.address, + token: weth, + user: supplier, + }); + const usdcWethFee = 500; + const usdcWethTickSpacing = usdcWethFee / 50; + const usdcWethInitialPrice = encodeSqrtRatioX96( + 1000000000000000000, + 1091760000 + ); + const usdcWethLowerPrice = encodeSqrtRatioX96( + 100000000000000000, + 1091760000 + ); + const usdcWethUpperPrice = encodeSqrtRatioX96( + 10000000000000000000, + 1091760000 + ); + await createNewPool({ + positionManager: nft, + token0: usdc, + token1: weth, + fee: usdcWethFee, + initialSqrtPrice: usdcWethInitialPrice.toString(), + }); + await mintNewPosition({ + nft: nft, + token0: usdc, + token1: weth, + fee: usdcWethFee, + user: supplier, + tickSpacing: usdcWethTickSpacing, + lowerPrice: usdcWethLowerPrice, + upperPrice: usdcWethUpperPrice, + token0Amount: userUsdcAmount, + token1Amount: userWethAmount, + }); + + return testEnv; + }; + + it("TC-erc721-buy-01: ERC721 <=> ERC20 via paraspace (1% platform fee) - partial borrow and swap", async () => { + const { + mayc, + usdc, + weth, + pWETH, + conduit, + conduitKey, + pausableZone, + seaport, + pool, + variableDebtWeth, + users: [maker, taker, middleman, platform], + } = await loadFixture(fixture); + const payNowNumber = "800"; + const creditNumber = "200"; + const payNowAmount = await convertToCurrencyDecimals( + usdc.address, + payNowNumber + ); + const creditAmount = await convertToCurrencyDecimals( + usdc.address, + creditNumber + ); + const startAmount = payNowAmount.add(creditAmount); + const endAmount = startAmount; + const nftId = "0"; + + // mint USDC to offer + await mintAndValidate(usdc, payNowNumber, taker); + // middleman supplies USDC to pool to be borrowed by offer later + await supplyAndValidate(usdc, creditNumber, middleman, true); + // maker mint mayc + await mintAndValidate(mayc, "1", maker); + // approve + await waitForTx( + await mayc.connect(maker.signer).approve(conduit.address, nftId) + ); + await waitForTx( + await usdc.connect(taker.signer).approve(conduit.address, startAmount) + ); + + //before buyWithCredit there is no collateral + const totalCollateralBase = (await pool.getUserAccountData(taker.address)) + .availableBorrowsBase; + expect(totalCollateralBase).to.be.equal(0); + + const getSellOrder = async (): Promise => { + const offers = [ + getOfferOrConsiderationItem(2, mayc.address, nftId, toBN(1), toBN(1)), + ]; + + const considerations = [ + getOfferOrConsiderationItem( + 1, + usdc.address, + toBN(0), + startAmount.sub(startAmount.div(100)), + endAmount.sub(startAmount.div(100)), + maker.address + ), + getOfferOrConsiderationItem( + 1, + usdc.address, + toBN(0), + startAmount.div(100), + endAmount.div(100), + platform.address + ), + ]; + return createSeaportOrder( + seaport, + maker, + offers, + considerations, + 2, + pausableZone.address, + conduitKey + ); + }; + const getBuyOrder = async (): Promise => { + const offers = [ + getOfferOrConsiderationItem( + 1, + usdc.address, + toBN(0), + startAmount, + endAmount + ), + ]; + + const considerations = [ + getOfferOrConsiderationItem( + 2, + mayc.address, + nftId, + toBN(1), + toBN(1), + pool.address + ), + ]; + return createSeaportOrder( + seaport, + taker, + offers, + considerations, + 2, + pausableZone.address, + conduitKey + ); + }; + + const fulfillment = [ + [[[0, 0]], [[1, 0]]], + [[[1, 0]], [[0, 0]]], + [[[1, 0]], [[0, 1]]], + ].map(([makerArr, considerationArr]) => + toFulfillment(makerArr, considerationArr) + ); + + const encodedData = seaport.interface.encodeFunctionData( + "matchAdvancedOrders", + [[await getSellOrder(), await getBuyOrder()], [], fulfillment] + ); + + const creditAmountInListingToken = creditAmount; + + const swapRouter = await getUniswapV3SwapRouter(); + const swapPayload = swapRouter.interface.encodeFunctionData("exactOutput", [ + { + path: solidityPack( + ["address", "uint24", "address"], + [usdc.address, 500, weth.address] + ), + recipient: pWETH.address, + deadline: 2659537628, + amountOut: creditAmountInListingToken, + amountInMaximum: MAX_UINT_AMOUNT, + }, + ]); + + const debtBefore = await variableDebtWeth.balanceOf(taker.address); + + await waitForTx( + await pool.connect(taker.signer).buyAnyWithCredit( + PARASPACE_SEAPORT_ID, + `0x${encodedData.slice(10)}`, + { + token: weth.address, + amount: creditAmountInListingToken, + orderId: constants.HashZero, + v: 0, + r: constants.HashZero, + s: constants.HashZero, + }, + UNISWAP_V3_SWAP_ADAPTER_ID, + `0x${swapPayload.slice(10)}`, + { + gasLimit: 5000000, + } + ) + ); + + expect(await mayc.balanceOf(taker.address)).to.be.equal(0); + expect(await mayc.ownerOf(nftId)).to.be.equal( + (await pool.getReserveData(mayc.address)).xTokenAddress + ); + + expect( + (await variableDebtWeth.balanceOf(taker.address)).sub(debtBefore) + ).eq("183313420582917789"); + }); +}); diff --git a/test/_pool_marketplace_buy_with_credit.spec.ts b/test/_pool_marketplace_buy_with_credit.spec.ts index d71315c16..aa6dccc50 100644 --- a/test/_pool_marketplace_buy_with_credit.spec.ts +++ b/test/_pool_marketplace_buy_with_credit.spec.ts @@ -11,6 +11,7 @@ import { getItemETH, getOfferOrConsiderationItem, toBN, + toFulfillment, } from "../helpers/seaport-helpers/encoding"; import {createX2Y2Order, createRunput} from "../helpers/x2y2-helpers"; import { @@ -22,9 +23,9 @@ import { } from "@looksrare/sdk"; import { LOOKSRARE_ID, - MAX_UINT_AMOUNT, PARASPACE_SEAPORT_ID, X2Y2_ID, + ZERO_ADDRESS, } from "../helpers/constants"; import {parseEther, splitSignature} from "ethers/lib/utils"; import {BigNumber, BigNumberish, constants} from "ethers"; @@ -48,6 +49,7 @@ import { import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; import {testEnvFixture} from "./helpers/setup-env"; import {AdvancedOrderStruct} from "../types/contracts/dependencies/seaport/contracts/Seaport"; +import {deployMintableERC721} from "../helpers/contracts-deployments"; describe("Leveraged Buy - Positive tests", () => { it("TC-erc721-buy-01: ERC721 <=> ERC20 via seaport - no loan", async () => { @@ -158,7 +160,7 @@ describe("Leveraged Buy - Positive tests", () => { await mayc.connect(maker.signer).approve(conduit.address, nftId) ); await waitForTx( - await usdc.connect(taker.signer).approve(pool.address, payNowAmount) + await usdc.connect(taker.signer).approve(conduit.address, startAmount) ); //before buyWithCredit there is no collateral @@ -189,6 +191,7 @@ describe("Leveraged Buy - Positive tests", () => { platform.address ), ]; + return createSeaportOrder( seaport, maker, @@ -199,9 +202,50 @@ describe("Leveraged Buy - Positive tests", () => { conduitKey ); }; + const getBuyOrder = async (): Promise => { + const offers = [ + getOfferOrConsiderationItem( + 1, + usdc.address, + toBN(0), + startAmount, + endAmount + ), + ]; + + const considerations = [ + getOfferOrConsiderationItem( + 2, + mayc.address, + nftId, + toBN(1), + toBN(1), + pool.address + ), + ]; + + return createSeaportOrder( + seaport, + taker, + offers, + considerations, + 2, + pausableZone.address, + conduitKey + ); + }; + + const fulfillment = [ + [[[0, 0]], [[1, 0]]], + [[[1, 0]], [[0, 0]]], + [[[1, 0]], [[0, 1]]], + ].map(([makerArr, considerationArr]) => + toFulfillment(makerArr, considerationArr) + ); + const encodedData = seaport.interface.encodeFunctionData( - "fulfillAdvancedOrder", - [await getSellOrder(), [], conduitKey, pool.address] + "matchAdvancedOrders", + [[await getSellOrder(), await getBuyOrder()], [], fulfillment] ); await waitForTx( @@ -216,7 +260,6 @@ describe("Leveraged Buy - Positive tests", () => { r: constants.HashZero, s: constants.HashZero, }, - 0, { gasLimit: 5000000, } @@ -450,24 +493,55 @@ describe("Leveraged Buy - Positive tests", () => { ); }; + const getBuyOrder = async (): Promise => { + const offers = [getItemETH(startAmount, startAmount)]; + const considerations = [ + getOfferOrConsiderationItem( + 2, + mayc.address, + nftId, + toBN(1), + toBN(1), + pool.address + ), + ]; + return createSeaportOrder( + seaport, + taker, + offers, + considerations, + 2, + pausableZone.address, + conduitKey + ); + }; + + const fulfillment = [ + [[[0, 0]], [[1, 0]]], + [[[1, 0]], [[0, 0]]], + [[[1, 0]], [[0, 1]]], + ].map(([makerArr, considerationArr]) => + toFulfillment(makerArr, considerationArr) + ); + const encodedData = seaport.interface.encodeFunctionData( - "fulfillAdvancedOrder", - [await getSellOrder(), [], conduitKey, pool.address] + "matchAdvancedOrders", + [[await getSellOrder(), await getBuyOrder()], [], fulfillment] ); + const offerBeforeBalance = await taker.signer.getBalance(); const txReceipt = await waitForTx( await pool.connect(taker.signer).buyWithCredit( PARASPACE_SEAPORT_ID, `0x${encodedData.slice(10)}`, { - token: constants.AddressZero, + token: weth.address, amount: creditAmount, orderId: constants.HashZero, v: 0, r: constants.HashZero, s: constants.HashZero, }, - 0, { gasLimit: 5000000, value: payNowAmount.add(refundAmount), @@ -535,7 +609,7 @@ describe("Leveraged Buy - Positive tests", () => { await mayc.connect(maker.signer).approve(conduit.address, nftId) ); await waitForTx( - await weth.connect(taker.signer).approve(pool.address, payNowAmount) + await weth.connect(taker.signer).approve(conduit.address, startAmount) ); const oldOfferBalance = await weth.balanceOf(maker.address); const oldPlatformBalance = await weth.balanceOf(platform.address); @@ -573,10 +647,50 @@ describe("Leveraged Buy - Positive tests", () => { ); }; + const getBuyOrder = async (): Promise => { + const offers = [ + getOfferOrConsiderationItem( + 1, + weth.address, + toBN(0), + startAmount, + startAmount + ), + ]; + const considerations = [ + getOfferOrConsiderationItem( + 2, + mayc.address, + nftId, + toBN(1), + toBN(1), + pool.address + ), + ]; + return createSeaportOrder( + seaport, + taker, + offers, + considerations, + 2, + pausableZone.address, + conduitKey + ); + }; + + const fulfillment = [ + [[[0, 0]], [[1, 0]]], + [[[1, 0]], [[0, 0]]], + [[[1, 0]], [[0, 1]]], + ].map(([makerArr, considerationArr]) => + toFulfillment(makerArr, considerationArr) + ); + const encodedData = seaport.interface.encodeFunctionData( - "fulfillAdvancedOrder", - [await getSellOrder(), [], conduitKey, pool.address] + "matchAdvancedOrders", + [[await getSellOrder(), await getBuyOrder()], [], fulfillment] ); + const offerBeforeBalance = await taker.signer.getBalance(); const txReceipt = await waitForTx( await pool.connect(taker.signer).buyWithCredit( @@ -590,7 +704,6 @@ describe("Leveraged Buy - Positive tests", () => { r: constants.HashZero, s: constants.HashZero, }, - 0, { gasLimit: 5000000, value: payNowAmount, @@ -688,19 +801,83 @@ describe("Leveraged Buy - Positive tests", () => { ); }; - const getEncodedData = (order: AdvancedOrderStruct): string => + const getBuyOrder = async ( + token: string, + nftId: BigNumberish, + listPrice: BigNumberish + ): Promise => { + const offers = [getItemETH(listPrice, listPrice)]; + const considerations = [ + getOfferOrConsiderationItem( + 2, + token, + nftId, + toBN(1), + toBN(1), + pool.address + ), + ]; + return createSeaportOrder( + seaport, + taker, + offers, + considerations, + 2, + pausableZone.address, + conduitKey + ); + }; + + const fulfillment = [ + [[[0, 0]], [[1, 0]]], + [[[1, 0]], [[0, 0]]], + ].map(([makerArr, considerationArr]) => + toFulfillment(makerArr, considerationArr) + ); + + const getEncodedData = ( + sellOrder: AdvancedOrderStruct, + buyOrder: AdvancedOrderStruct + ): string => `0x${seaport.interface - .encodeFunctionData("fulfillAdvancedOrder", [ - order, + .encodeFunctionData("matchAdvancedOrders", [ + [sellOrder, buyOrder], [], - conduitKey, - pool.address, + fulfillment, ]) .slice(10)}`; - const orderETH0 = await getSellOrder(mayc.address, nftIds[0], startAmount); - const orderETH1 = await getSellOrder(mayc.address, nftIds[1], startAmount); - const orderETH2 = await getSellOrder(mayc.address, nftIds[2], startAmount); + const sellOrderETH0 = await getSellOrder( + mayc.address, + nftIds[0], + startAmount + ); + const sellOrderETH1 = await getSellOrder( + mayc.address, + nftIds[1], + startAmount + ); + const sellOrderETH2 = await getSellOrder( + mayc.address, + nftIds[2], + startAmount + ); + + const buyOrderETH0 = await getBuyOrder( + mayc.address, + nftIds[0], + startAmount + ); + const buyOrderETH1 = await getBuyOrder( + mayc.address, + nftIds[1], + startAmount + ); + const buyOrderETH2 = await getBuyOrder( + mayc.address, + nftIds[2], + startAmount + ); const emptySig = { orderId: constants.HashZero, @@ -710,17 +887,17 @@ describe("Leveraged Buy - Positive tests", () => { }; const creditETH0 = { - token: constants.AddressZero, + token: weth.address, amount: creditAmount, ...emptySig, }; const creditETH1 = { - token: constants.AddressZero, + token: weth.address, amount: creditAmount, ...emptySig, }; const creditETH2 = { - token: constants.AddressZero, + token: weth.address, amount: creditAmount, ...emptySig, }; @@ -739,12 +916,11 @@ describe("Leveraged Buy - Positive tests", () => { .batchBuyWithCredit( [PARASPACE_SEAPORT_ID, PARASPACE_SEAPORT_ID, PARASPACE_SEAPORT_ID], [ - getEncodedData(orderETH0), - getEncodedData(orderETH1), - getEncodedData(orderETH2), + getEncodedData(sellOrderETH0, buyOrderETH0), + getEncodedData(sellOrderETH1, buyOrderETH1), + getEncodedData(sellOrderETH2, buyOrderETH2), ], [creditETH0, creditETH1, creditETH2], - 0, { gasLimit: 5000000, value: totalPayNowAmount, @@ -858,19 +1034,93 @@ describe("Leveraged Buy - Positive tests", () => { ); }; - const getEncodedData = (order: AdvancedOrderStruct): string => + const getBuyOrder = async ( + token: string, + nftId: BigNumberish, + listPrice: BigNumberish, + listInETH = true + ): Promise => { + const offers = [ + listInETH + ? getItemETH(listPrice, listPrice) + : getOfferOrConsiderationItem( + 1, + weth.address, + toBN(0), + listPrice, + listPrice + ), + ]; + + const considerations = [ + getOfferOrConsiderationItem( + 2, + token, + nftId, + toBN(1), + toBN(1), + pool.address + ), + ]; + + return createSeaportOrder( + seaport, + taker, + offers, + considerations, + 2, + pausableZone.address, + conduitKey + ); + }; + + const fulfillment = [ + [[[0, 0]], [[1, 0]]], + [[[1, 0]], [[0, 0]]], + ].map(([makerArr, considerationArr]) => + toFulfillment(makerArr, considerationArr) + ); + + const getEncodedData = ( + sellOrder: AdvancedOrderStruct, + buyOrder: AdvancedOrderStruct + ): string => `0x${seaport.interface - .encodeFunctionData("fulfillAdvancedOrder", [ - order, + .encodeFunctionData("matchAdvancedOrders", [ + [sellOrder, buyOrder], [], - conduitKey, - pool.address, + fulfillment, ]) .slice(10)}`; - const orderETH0 = await getSellOrder(mayc.address, nftIds[0], startAmount); - const orderETH1 = await getSellOrder(mayc.address, nftIds[1], startAmount); - const orderWETH2 = await getSellOrder( + const sellOrderETH0 = await getSellOrder( + mayc.address, + nftIds[0], + startAmount + ); + const sellOrderETH1 = await getSellOrder( + mayc.address, + nftIds[1], + startAmount + ); + const sellOrderWETH2 = await getSellOrder( + mayc.address, + nftIds[2], + startAmount, + false + ); + + const buyOrderETH0 = await getBuyOrder( + mayc.address, + nftIds[0], + startAmount + ); + const buyOrderETH1 = await getBuyOrder( + mayc.address, + nftIds[1], + startAmount + ); + const buyOrderWETH2 = await getBuyOrder( mayc.address, nftIds[2], startAmount, @@ -885,12 +1135,12 @@ describe("Leveraged Buy - Positive tests", () => { }; const creditETH0 = { - token: constants.AddressZero, + token: weth.address, amount: creditAmount, ...emptySig, }; const creditETH1 = { - token: constants.AddressZero, + token: weth.address, amount: creditAmount, ...emptySig, }; @@ -907,7 +1157,7 @@ describe("Leveraged Buy - Positive tests", () => { await waitForTx( await weth .connect(taker.signer) - .approve(pool.address, totalPayNowAmountInWETH) + .approve(conduit.address, totalPayNowAmountInWETH.add(creditAmount)) ); // batchBuyWithCredit([ETH, WETH, ETH]) will fail @@ -917,18 +1167,17 @@ describe("Leveraged Buy - Positive tests", () => { .batchBuyWithCredit( [PARASPACE_SEAPORT_ID, PARASPACE_SEAPORT_ID, PARASPACE_SEAPORT_ID], [ - getEncodedData(orderETH0), - getEncodedData(orderWETH2), - getEncodedData(orderETH1), + getEncodedData(sellOrderETH0, buyOrderETH0), + getEncodedData(sellOrderWETH2, buyOrderWETH2), + getEncodedData(sellOrderETH1, buyOrderETH1), ], [creditETH0, creditWETH2, creditETH1], - 0, { gasLimit: 5000000, - value: totalPayNowAmount, + value: totalPayNowAmountInETH, } ) - ).to.revertedWith(ProtocolErrors.PAYNOW_NOT_ENOUGH); + ).to.reverted; const makerETHBeforeBalance = await maker.signer.getBalance(); const takerETHBeforeBalance = await taker.signer.getBalance(); @@ -939,12 +1188,11 @@ describe("Leveraged Buy - Positive tests", () => { .batchBuyWithCredit( [PARASPACE_SEAPORT_ID, PARASPACE_SEAPORT_ID, PARASPACE_SEAPORT_ID], [ - getEncodedData(orderETH0), - getEncodedData(orderETH1), - getEncodedData(orderWETH2), + getEncodedData(sellOrderETH0, buyOrderETH0), + getEncodedData(sellOrderETH1, buyOrderETH1), + getEncodedData(sellOrderWETH2, buyOrderWETH2), ], [creditETH0, creditETH1, creditWETH2], - 0, { gasLimit: 5000000, value: totalPayNowAmount, @@ -1223,7 +1471,7 @@ describe("Leveraged Buy - Positive tests", () => { [`0x${encodedData.slice(10)}`, `0x${encodedDataX2Y2.slice(10)}`], [ { - token: constants.AddressZero, + token: weth.address, amount: payLaterAmount, orderId: constants.HashZero, v: 0, @@ -1239,7 +1487,6 @@ describe("Leveraged Buy - Positive tests", () => { s: constants.HashZero, }, ], - 0, { gasLimit: 5000000, value: payNowAmount, @@ -1484,7 +1731,7 @@ describe("Leveraged Buy - Positive tests", () => { const nftId = 0; // mint USDC to taker and middleman - await mintAndValidate(usdc, payNowNumber, taker); + await supplyAndValidate(usdc, payNowNumber, taker, true); // middleman supplies USDC to pool to be borrowed by offer later await supplyAndValidate(usdc, creditNumber, middleman, true); await supplyAndValidate(usdc, borrowAmount, middleman, true); @@ -1498,20 +1745,18 @@ describe("Leveraged Buy - Positive tests", () => { await borrowAndValidate(usdc, borrowAmount, maker); await waitForTx( - await usdc.connect(maker.signer).approve(pool.address, MAX_UINT_AMOUNT) - ); - await waitForTx( - await usdc.connect(taker.signer).approve(pool.address, startAmount) + await pUsdc.connect(taker.signer).approve(pool.address, startAmount) ); await executeSeaportBuyWithCredit( nBAYC, - usdc, + pUsdc, startAmount, endAmount, creditAmount, nftId, maker, - taker + taker, + true ); const usdcConfigData = BigNumber.from( @@ -1522,21 +1767,413 @@ describe("Leveraged Buy - Positive tests", () => { expect(await nBAYC.collateralizedBalanceOf(taker.address)).to.be.equal(1); assertAlmostEqual( await pUsdc.balanceOf(maker.address), - startAmount - .percentMul("9500") - .add(await convertToCurrencyDecimals(usdc.address, "1")) // default supply ratio + startAmount.add(await convertToCurrencyDecimals(usdc.address, "1")) ); assertAlmostEqual( await usdc.balanceOf(maker.address), - startAmount - .percentMul("500") - .add(await convertToCurrencyDecimals(usdc.address, borrowAmount)) + + await convertToCurrencyDecimals(usdc.address, borrowAmount) ); expect(isUsingAsCollateral(usdcConfigData, usdcReserveData.id)).to.be.true; expect( (await pool.getUserAccountData(maker.address)).totalDebtBase ).to.be.gt(0); // no debt repaid }); + + it("TC-erc721-buy-27: ETH <=> NToken via ParaSpace - partial borrow & pToken minted", async () => { + const { + bayc, + nBAYC, + weth, + pWETH, + pool, + conduit, + seaport, + pausableZone, + conduitKey, + users: [maker, taker, middleman], + } = await loadFixture(testEnvFixture); + const payNowNumber = "80"; + const creditNumber = "20"; + const payNowAmount = await convertToCurrencyDecimals( + weth.address, + payNowNumber + ); + const creditAmount = await convertToCurrencyDecimals( + weth.address, + creditNumber + ); + const startAmount = payNowAmount.add(creditAmount); + const endAmount = startAmount; + const borrowNumber = "20"; + const nftId = 0; + + // mint WETH to taker and middleman + await supplyAndValidate(weth, payNowNumber, taker, true); + // middleman supplies WETH to pool to be borrowed by offer later + await supplyAndValidate(weth, creditNumber, middleman, true); + await supplyAndValidate(weth, borrowNumber, middleman, true); + + await waitForTx( + await middleman.signer.sendTransaction({ + to: weth.address, + value: creditAmount.add(borrowNumber), + }) + ); + + await supplyAndValidate(bayc, "1", maker, true); + await borrowAndValidate(weth, borrowNumber, maker); + + await waitForTx( + await nBAYC.connect(maker.signer).approve(conduit.address, nftId) + ); + await waitForTx( + await pWETH.connect(taker.signer).approve(conduit.address, startAmount) + ); + + const getSellOrder = async (): Promise => { + const offers = [ + getOfferOrConsiderationItem(2, bayc.address, nftId, 1, 1), + ]; + + const considerations = [ + getOfferOrConsiderationItem( + 1, + pWETH.address, + toBN(0), + startAmount, + endAmount, + pool.address + ), + ]; + + return createSeaportOrder( + seaport, + maker, + offers, + considerations, + 2, + pausableZone.address, + conduitKey + ); + }; + + const getBuyOrder = async (): Promise => { + const offers = [ + getOfferOrConsiderationItem( + 1, + pWETH.address, + toBN(0), + startAmount, + endAmount + ), + ]; + + const considerations = [ + getOfferOrConsiderationItem(2, bayc.address, nftId, 1, 1, pool.address), + ]; + + return createSeaportOrder( + seaport, + taker, + offers, + considerations, + 2, + pausableZone.address, + conduitKey + ); + }; + + const fulfillment = [ + [[[0, 0]], [[1, 0]]], + [[[1, 0]], [[0, 0]]], + ].map(([makerArr, considerationArr]) => + toFulfillment(makerArr, considerationArr) + ); + + const encodedData = seaport.interface.encodeFunctionData( + "matchAdvancedOrders", + [[await getSellOrder(), await getBuyOrder()], [], fulfillment] + ); + + await waitForTx( + await pool + .connect(taker.signer) + .buyWithCredit(PARASPACE_SEAPORT_ID, `0x${encodedData.slice(10)}`, { + token: weth.address, + amount: creditAmount, + orderId: constants.HashZero, + v: 0, + r: constants.HashZero, + s: constants.HashZero, + }) + ); + + const wethConfigData = BigNumber.from( + (await pool.getUserConfiguration(maker.address)).data + ); + const wethReserveData = await pool.getReserveData(weth.address); + expect(await nBAYC.ownerOf(nftId)).to.be.equal(taker.address); + expect(await nBAYC.collateralizedBalanceOf(taker.address)).to.be.equal(1); + assertAlmostEqual(await pWETH.balanceOf(maker.address), startAmount); + assertAlmostEqual( + await weth.balanceOf(maker.address), + await convertToCurrencyDecimals(weth.address, borrowNumber) + ); + expect(isUsingAsCollateral(wethConfigData, wethReserveData.id)).to.be.true; + }); + + it("TC-erc721-buy-28: ETH <=> ANY NFT via ParaSpace - no borrow", async () => { + const { + weth, + pool, + conduit, + seaport, + pausableZone, + conduitKey, + users: [maker, taker], + } = await loadFixture(testEnvFixture); + const nft = await deployMintableERC721( + ["test_TC_erc721_buy_28", "test_TC_erc721_buy_28", ""], + false + ); + const payNowNumber = "100"; + const creditNumber = "0"; + const payNowAmount = await convertToCurrencyDecimals( + weth.address, + payNowNumber + ); + const creditAmount = await convertToCurrencyDecimals( + weth.address, + creditNumber + ); + const startAmount = payNowAmount.add(creditAmount); + const endAmount = startAmount; + const nftId = 0; + + await mintAndValidate(weth, payNowNumber, taker); + await mintAndValidate(nft, "1", maker); + + await waitForTx( + await nft.connect(maker.signer).approve(conduit.address, nftId) + ); + + const getSellOrder = async (): Promise => { + const offers = [getOfferOrConsiderationItem(2, nft.address, nftId, 1, 1)]; + + const considerations = [getItemETH(startAmount, endAmount, pool.address)]; + + return createSeaportOrder( + seaport, + maker, + offers, + considerations, + 2, + pausableZone.address, + conduitKey + ); + }; + + const getBuyOrder = async (): Promise => { + const offers = [getItemETH(startAmount, endAmount)]; + + const considerations = [ + getOfferOrConsiderationItem(2, nft.address, nftId, 1, 1, pool.address), + ]; + + return createSeaportOrder( + seaport, + taker, + offers, + considerations, + 2, + pausableZone.address, + conduitKey + ); + }; + + const fulfillment = [ + [[[0, 0]], [[1, 0]]], + [[[1, 0]], [[0, 0]]], + ].map(([makerArr, considerationArr]) => + toFulfillment(makerArr, considerationArr) + ); + + const encodedData = seaport.interface.encodeFunctionData( + "matchAdvancedOrders", + [[await getSellOrder(), await getBuyOrder()], [], fulfillment] + ); + + await waitForTx( + await pool.connect(taker.signer).buyWithCredit( + PARASPACE_SEAPORT_ID, + `0x${encodedData.slice(10)}`, + { + token: ZERO_ADDRESS, + amount: creditAmount, + orderId: constants.HashZero, + v: 0, + r: constants.HashZero, + s: constants.HashZero, + }, + { + value: payNowAmount, + gasLimit: 5000000, + } + ) + ); + + expect(await nft.ownerOf(nftId)).to.be.equal(taker.address); + }); + + it("TC-erc721-buy-29: ETH <=> NToken via ParaSpace - partial borrow & pToken minted & private listing", async () => { + const { + bayc, + nBAYC, + weth, + pWETH, + pool, + conduit, + seaport, + pausableZone, + conduitKey, + users: [maker, taker, middleman], + } = await loadFixture(testEnvFixture); + const payNowNumber = "80"; + const creditNumber = "20"; + const payNowAmount = await convertToCurrencyDecimals( + weth.address, + payNowNumber + ); + const creditAmount = await convertToCurrencyDecimals( + weth.address, + creditNumber + ); + const startAmount = payNowAmount.add(creditAmount); + const endAmount = startAmount; + const borrowNumber = "20"; + const nftId = 0; + + // mint WETH to taker and middleman + await supplyAndValidate(weth, payNowNumber, taker, true); + // middleman supplies WETH to pool to be borrowed by offer later + await supplyAndValidate(weth, creditNumber, middleman, true); + await supplyAndValidate(weth, borrowNumber, middleman, true); + + await waitForTx( + await middleman.signer.sendTransaction({ + to: weth.address, + value: creditAmount.add(borrowNumber), + }) + ); + + await supplyAndValidate(bayc, "1", maker, true); + await borrowAndValidate(weth, borrowNumber, maker); + + await waitForTx( + await nBAYC.connect(maker.signer).approve(conduit.address, nftId) + ); + await waitForTx( + await pWETH.connect(taker.signer).approve(conduit.address, startAmount) + ); + + const getSellOrder = async (): Promise => { + const offers = [ + getOfferOrConsiderationItem(2, bayc.address, nftId, 1, 1), + ]; + + const considerations = [ + getOfferOrConsiderationItem( + 1, + pWETH.address, + toBN(0), + startAmount, + endAmount, + pool.address + ), + getOfferOrConsiderationItem( + 2, + bayc.address, + nftId, + 1, + 1, + taker.address + ), + ]; + + return createSeaportOrder( + seaport, + maker, + offers, + considerations, + 2, + pausableZone.address, + conduitKey + ); + }; + + const getBuyOrder = async (): Promise => { + const offers = [ + getOfferOrConsiderationItem( + 1, + pWETH.address, + toBN(0), + startAmount, + endAmount + ), + ]; + + const considerations = []; + + return createSeaportOrder( + seaport, + taker, + offers, + considerations, + 2, + pausableZone.address, + conduitKey + ); + }; + + const fulfillment = [ + [[[0, 0]], [[0, 1]]], + [[[1, 0]], [[0, 0]]], + ].map(([makerArr, considerationArr]) => + toFulfillment(makerArr, considerationArr) + ); + + const encodedData = seaport.interface.encodeFunctionData( + "matchAdvancedOrders", + [[await getSellOrder(), await getBuyOrder()], [], fulfillment] + ); + + await waitForTx( + await pool + .connect(taker.signer) + .buyWithCredit(PARASPACE_SEAPORT_ID, `0x${encodedData.slice(10)}`, { + token: weth.address, + amount: creditAmount, + orderId: constants.HashZero, + v: 0, + r: constants.HashZero, + s: constants.HashZero, + }) + ); + + const wethConfigData = BigNumber.from( + (await pool.getUserConfiguration(maker.address)).data + ); + const wethReserveData = await pool.getReserveData(weth.address); + expect(await nBAYC.ownerOf(nftId)).to.be.equal(taker.address); + expect(await nBAYC.collateralizedBalanceOf(taker.address)).to.be.equal(1); + assertAlmostEqual(await pWETH.balanceOf(maker.address), startAmount); + assertAlmostEqual( + await weth.balanceOf(maker.address), + await convertToCurrencyDecimals(weth.address, borrowNumber) + ); + expect(isUsingAsCollateral(wethConfigData, wethReserveData.id)).to.be.true; + }); }); describe("Leveraged Buy - Negative tests", () => { @@ -1732,7 +2369,7 @@ describe("Leveraged Buy - Negative tests", () => { maker, taker ) - ).to.be.revertedWith(ProtocolErrors.ASSET_NOT_LISTED); + ).to.be.reverted; }); it("TC-erc721-buy-21: Cannot pay later an amount above the NFT's LTV", async () => { diff --git a/test/_uniswapv3_pool_operation.spec.ts b/test/_uniswapv3_pool_operation.spec.ts index 3b289bc35..473dfaa1c 100644 --- a/test/_uniswapv3_pool_operation.spec.ts +++ b/test/_uniswapv3_pool_operation.spec.ts @@ -720,7 +720,7 @@ describe("Uniswap V3 NFT supply, withdraw, setCollateral, liquidation and transf ); }); - it("UniswapV3 asset can be auctioned [ @skip-on-coverage ]", async () => { + it("UniswapV3 asset cannot be auctioned [ @skip-on-coverage ]", async () => { const { users: [borrower, liquidator], pool, @@ -746,13 +746,11 @@ describe("Uniswap V3 NFT supply, withdraw, setCollateral, liquidation and transf expect(liquidatorBalance).to.eq(0); // try to start auction - await waitForTx( - await pool + await expect( + pool .connect(liquidator.signer) .startAuction(borrower.address, nftPositionManager.address, 1) - ); - - expect(await nUniswapV3.isAuctioned(1)).to.be.true; + ).to.be.revertedWith(ProtocolErrors.AUCTION_NOT_ENABLED); }); it("liquidation failed if underlying erc20 was not active [ @skip-on-coverage ]", async () => { diff --git a/test/helpers/make-suite.ts b/test/helpers/make-suite.ts index 6f6aa9f65..fc6bbf9df 100644 --- a/test/helpers/make-suite.ts +++ b/test/helpers/make-suite.ts @@ -162,6 +162,7 @@ export interface TestEnv { pcETH: PToken; dai: MintableERC20; pDai: PToken; + variableDebtUsdc: VariableDebtToken; variableDebtDai: VariableDebtToken; variableDebtStETH: StETHDebtToken; variableDebtAWeth: ATokenDebtToken; @@ -243,6 +244,7 @@ export async function initializeMakeSuite() { pcETH: {} as PToken, dai: {} as MintableERC20, pDai: {} as PToken, + variableDebtUsdc: {} as VariableDebtToken, variableDebtDai: {} as VariableDebtToken, variableDebtWeth: {} as VariableDebtToken, pUsdc: {} as PToken, @@ -443,7 +445,10 @@ export async function initializeMakeSuite() { await testEnv.protocolDataProvider.getReserveTokensAddresses( wethAddress || "" ); - + const {variableDebtTokenAddress: variableDebtUsdcAddress} = + await testEnv.protocolDataProvider.getReserveTokensAddresses( + usdcAddress || "" + ); const aWETHAddress = reservesTokens.find( (token) => token.symbol === ERC20TokenContractId.aWETH )?.tokenAddress; @@ -460,7 +465,6 @@ export async function initializeMakeSuite() { const punksAddress = reservesTokens.find( (token) => token.symbol === eContractid.PUNKS )?.tokenAddress; - const wpunkAddress = reservesTokens.find( (token) => token.symbol === ERC721TokenContractId.WPUNKS )?.tokenAddress; @@ -505,6 +509,9 @@ export async function initializeMakeSuite() { } testEnv.pDai = await getPToken(pDaiAddress); + testEnv.variableDebtUsdc = await getVariableDebtToken( + variableDebtUsdcAddress + ); testEnv.variableDebtDai = await getVariableDebtToken(variableDebtDaiAddress); testEnv.variableDebtWeth = await getVariableDebtToken( variableDebtWethAddress diff --git a/test/helpers/marketplace-helper.ts b/test/helpers/marketplace-helper.ts index 6ca9e9569..97f228dcf 100644 --- a/test/helpers/marketplace-helper.ts +++ b/test/helpers/marketplace-helper.ts @@ -32,6 +32,7 @@ import { MintableERC20, MintableERC721, NToken, + PToken, WETH9, WETH9Mocked, } from "../../types"; @@ -144,7 +145,6 @@ export async function executeLooksrareBuyWithCredit( r: constants.HashZero, s: constants.HashZero, }, - 0, { gasLimit: 5000000, } @@ -223,7 +223,6 @@ export async function executeBlurBuyWithCredit( r: constants.HashZero, s: constants.HashZero, }, - 0, { gasLimit: 5000000, } @@ -287,7 +286,6 @@ export async function executeX2Y2BuyWithCredit( r: constants.HashZero, s: constants.HashZero, }, - 0, { gasLimit: 5000000, } @@ -297,42 +295,42 @@ export async function executeX2Y2BuyWithCredit( export async function executeSeaportBuyWithCredit( tokenToBuy: MintableERC721 | NToken, - tokenToPayWith: MintableERC20, + tokenToPayWith: MintableERC20 | PToken, startAmount: BigNumberish, endAmount: BigNumberish, payLaterAmount: BigNumberish, nftId: number, maker: SignerWithAddress, taker: SignerWithAddress, - _sellOrderStartAmount = 1 + isCollateralSwap = false ) { + const pool = await getPoolProxy(); // approve await waitForTx( await tokenToBuy .connect(maker.signer) .approve((await getConduit()).address, nftId) ); + await waitForTx( + await tokenToPayWith + .connect(taker.signer) + .approve((await getConduit()).address, startAmount) + ); const seaport = await getSeaport(); const getSellOrder = async (): Promise => { const offers = [ - getOfferOrConsiderationItem( - 2, - tokenToBuy.address, - nftId, - _sellOrderStartAmount, - _sellOrderStartAmount - ), + getOfferOrConsiderationItem(2, tokenToBuy.address, nftId, 1, 1), ]; const considerations = [ getOfferOrConsiderationItem( 1, tokenToPayWith.address, - nftId, + 0, startAmount, endAmount, - maker.address + isCollateralSwap ? pool.address : maker.address ), ]; @@ -347,25 +345,64 @@ export async function executeSeaportBuyWithCredit( ); }; - const pool = await getPoolProxy(); + const getBuyOrder = async (): Promise => { + const offers = [ + getOfferOrConsiderationItem( + 1, + tokenToPayWith.address, + 0, + startAmount, + endAmount + ), + ]; + + const considerations = [ + getOfferOrConsiderationItem( + 2, + tokenToBuy.address, + nftId, + 1, + 1, + pool.address + ), + ]; + + return createSeaportOrder( + seaport, + taker, + offers, + considerations, + 2, + (await getPausableZone()).address, + await getConduitKey() + ); + }; + + const fulfillment = [ + [[[0, 0]], [[1, 0]]], + [[[1, 0]], [[0, 0]]], + ].map(([makerArr, considerationArr]) => + toFulfillment(makerArr, considerationArr) + ); const encodedData = seaport.interface.encodeFunctionData( - "fulfillAdvancedOrder", - [await getSellOrder(), [], await getConduitKey(), pool.address] + "matchAdvancedOrders", + [[await getSellOrder(), await getBuyOrder()], [], fulfillment] ); const tx = (await getPoolProxy()).connect(taker.signer).buyWithCredit( PARASPACE_SEAPORT_ID, `0x${encodedData.slice(10)}`, { - token: tokenToPayWith.address, + token: isCollateralSwap + ? await (tokenToPayWith as PToken).UNDERLYING_ASSET_ADDRESS() + : tokenToPayWith.address, amount: payLaterAmount, orderId: constants.HashZero, v: 0, r: constants.HashZero, s: constants.HashZero, }, - 0, { gasLimit: 5000000, } @@ -376,13 +413,14 @@ export async function executeSeaportBuyWithCredit( export async function executeAcceptBidWithCredit( tokenToBuy: MintableERC721 | NToken, - tokenToPayWith: MintableERC20, + tokenToPayWith: MintableERC20 | PToken, startAmount: BigNumberish, endAmount: BigNumberish, payLaterAmount: BigNumberish, nftId: number, maker: SignerWithAddress, - taker: SignerWithAddress + taker: SignerWithAddress, + isCollateralSwap = false ) { const pool = await getPoolProxy(); const seaport = await getSeaport(); @@ -451,7 +489,7 @@ export async function executeAcceptBidWithCredit( toBN(0), startAmount, endAmount, - taker.address + isCollateralSwap ? pool.address : taker.address ), ]; @@ -489,7 +527,9 @@ export async function executeAcceptBidWithCredit( }; const payLater = { - token: tokenToPayWith.address, + token: isCollateralSwap + ? await (tokenToPayWith as PToken).UNDERLYING_ASSET_ADDRESS() + : tokenToPayWith.address, amount: payLaterAmount, orderId: DRE.ethers.utils.arrayify(sellOrder.signature), }; @@ -510,7 +550,6 @@ export async function executeAcceptBidWithCredit( ...vrs, }, taker.address, - 0, { gasLimit: 5000000, }