From d69b27e6491ca187e31f7d92a763b9cc60e1405c Mon Sep 17 00:00:00 2001 From: GopherJ Date: Tue, 31 Jan 2023 22:16:57 +0800 Subject: [PATCH 01/19] feat: atomic ntoken Signed-off-by: GopherJ --- .../IAtomicCollateralizableERC721.sol | 35 ++ contracts/interfaces/INToken.sol | 2 - contracts/mocks/upgradeability/MockNToken.sol | 2 +- .../protocol/libraries/logic/GenericLogic.sol | 57 +-- .../libraries/logic/LiquidationLogic.sol | 26 +- contracts/protocol/tokenization/NToken.sol | 13 +- .../tokenization/NTokenApeStaking.sol | 2 +- .../protocol/tokenization/NTokenBAKC.sol | 2 +- .../protocol/tokenization/NTokenMoonBirds.sol | 2 +- .../protocol/tokenization/NTokenUniswapV3.sol | 15 +- .../base/MintableIncentivizedERC721.sol | 98 ++++- .../libraries/MintableERC721Logic.sol | 408 ++++++++++++++---- contracts/ui/UiPoolDataProvider.sol | 29 +- .../ui/interfaces/IUiPoolDataProvider.sol | 1 - 14 files changed, 524 insertions(+), 168 deletions(-) create mode 100644 contracts/interfaces/IAtomicCollateralizableERC721.sol diff --git a/contracts/interfaces/IAtomicCollateralizableERC721.sol b/contracts/interfaces/IAtomicCollateralizableERC721.sol new file mode 100644 index 000000000..5501900a3 --- /dev/null +++ b/contracts/interfaces/IAtomicCollateralizableERC721.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +/** + * @title IAtomicCollateralizableERC721 + * @author Parallel + * @notice Defines the basic interface for an CollateralizableERC721. + **/ +interface IAtomicCollateralizableERC721 { + /** + * @dev get the collateralized atomic token balance of a specific user + */ + function atomicCollateralizedBalanceOf(address user) + external + view + returns (uint256); + + /** + * @dev get the atomic token balance of a specific user + */ + function atomicBalanceOf(address user) external view returns (uint256); + + /** + * @dev check if specific token is atomic (has multiplier) + */ + function isAtomicToken(uint256 tokenId) external view returns (bool); + + /** + * @dev get the trait multiplier of specific token + */ + function getTraitMultiplier(uint256 tokenId) + external + view + returns (uint256); +} diff --git a/contracts/interfaces/INToken.sol b/contracts/interfaces/INToken.sol index a45150a5d..bc163aefc 100644 --- a/contracts/interfaces/INToken.sol +++ b/contracts/interfaces/INToken.sol @@ -187,6 +187,4 @@ interface INToken is address airdropContract, bytes calldata airdropParams ) external; - - function getAtomicPricingConfig() external view returns (bool); } diff --git a/contracts/mocks/upgradeability/MockNToken.sol b/contracts/mocks/upgradeability/MockNToken.sol index 66e23e9d7..2aa8b5923 100644 --- a/contracts/mocks/upgradeability/MockNToken.sol +++ b/contracts/mocks/upgradeability/MockNToken.sol @@ -6,7 +6,7 @@ import {IPool} from "../../interfaces/IPool.sol"; import {IRewardController} from "../../interfaces/IRewardController.sol"; contract MockNToken is NToken { - constructor(IPool pool) NToken(pool, false) {} + constructor(IPool pool) NToken(pool) {} function getRevision() internal pure override returns (uint256) { return 999; diff --git a/contracts/protocol/libraries/logic/GenericLogic.sol b/contracts/protocol/libraries/logic/GenericLogic.sol index 19be4c206..20f94e06a 100644 --- a/contracts/protocol/libraries/logic/GenericLogic.sol +++ b/contracts/protocol/libraries/logic/GenericLogic.sol @@ -7,6 +7,7 @@ import {Math} from "../../../dependencies/openzeppelin/contracts/Math.sol"; import {IScaledBalanceToken} from "../../../interfaces/IScaledBalanceToken.sol"; import {INToken} from "../../../interfaces/INToken.sol"; import {ICollateralizableERC721} from "../../../interfaces/ICollateralizableERC721.sol"; +import {IAtomicCollateralizableERC721} from "../../../interfaces/IAtomicCollateralizableERC721.sol"; import {IPriceOracleGetter} from "../../../interfaces/IPriceOracleGetter.sol"; import {ReserveConfiguration} from "../configuration/ReserveConfiguration.sol"; import {UserConfiguration} from "../configuration/UserConfiguration.sol"; @@ -364,35 +365,37 @@ library GenericLogic { CalculateUserAccountDataVars memory vars ) private view returns (uint256 totalValue) { INToken nToken = INToken(vars.xTokenAddress); - bool isAtomicPrice = nToken.getAtomicPricingConfig(); - if (isAtomicPrice) { - uint256 totalBalance = nToken.balanceOf(params.user); - - for (uint256 index = 0; index < totalBalance; index++) { - uint256 tokenId = nToken.tokenOfOwnerByIndex( - params.user, - index + uint256 collateralizedBalance = ICollateralizableERC721( + vars.xTokenAddress + ).collateralizedBalanceOf(params.user); + uint256 atomicCollateralizedBalance = IAtomicCollateralizableERC721( + vars.xTokenAddress + ).atomicCollateralizedBalanceOf(params.user); + uint256 balance = INToken(vars.xTokenAddress).balanceOf(params.user); + uint256 atomicBalance = IAtomicCollateralizableERC721( + vars.xTokenAddress + ).atomicBalanceOf(params.user); + uint256 assetPrice = _getAssetPrice( + params.oracle, + vars.currentReserveAddress + ); + totalValue = + (collateralizedBalance - atomicCollateralizedBalance) * + assetPrice; + + for (uint256 index = atomicBalance; index < balance; index++) { + uint256 tokenId = nToken.tokenOfOwnerByIndex(params.user, index); + if ( + ICollateralizableERC721(vars.xTokenAddress).isUsedAsCollateral( + tokenId + ) + ) { + totalValue += _getTokenPrice( + params.oracle, + vars.currentReserveAddress, + tokenId ); - if ( - ICollateralizableERC721(vars.xTokenAddress) - .isUsedAsCollateral(tokenId) - ) { - totalValue += _getTokenPrice( - params.oracle, - vars.currentReserveAddress, - tokenId - ); - } } - } else { - uint256 assetPrice = _getAssetPrice( - params.oracle, - vars.currentReserveAddress - ); - totalValue = - ICollateralizableERC721(vars.xTokenAddress) - .collateralizedBalanceOf(params.user) * - assetPrice; } } diff --git a/contracts/protocol/libraries/logic/LiquidationLogic.sol b/contracts/protocol/libraries/logic/LiquidationLogic.sol index 80a3482f5..2ec0d22a3 100644 --- a/contracts/protocol/libraries/logic/LiquidationLogic.sol +++ b/contracts/protocol/libraries/logic/LiquidationLogic.sol @@ -18,6 +18,7 @@ import {Address} from "../../../dependencies/openzeppelin/contracts/Address.sol" import {IPToken} from "../../../interfaces/IPToken.sol"; import {IWETH} from "../../../misc/interfaces/IWETH.sol"; import {ICollateralizableERC721} from "../../../interfaces/ICollateralizableERC721.sol"; +import {IAtomicCollateralizableERC721} from "../../../interfaces/IAtomicCollateralizableERC721.sol"; import {IAuctionableERC721} from "../../../interfaces/IAuctionableERC721.sol"; import {INToken} from "../../../interfaces/INToken.sol"; import {PRBMath} from "../../../dependencies/math/PRBMath.sol"; @@ -40,6 +41,7 @@ library LiquidationLogic { using UserConfiguration for DataTypes.UserConfigurationMap; using ReserveConfiguration for DataTypes.ReserveConfigurationMap; using PRBMathUD60x18 for uint256; + using WadRayMath for uint256; using GPv2SafeERC20 for IERC20; /** @@ -776,12 +778,24 @@ library LiquidationLogic { ).collateralizedBalanceOf(params.borrower); // price of the asset that is used as collateral - if (INToken(superVars.collateralXToken).getAtomicPricingConfig()) { - vars.collateralPrice = IPriceOracleGetter(params.priceOracle) - .getTokenPrice( - params.collateralAsset, - params.collateralTokenId - ); + if ( + IAtomicCollateralizableERC721(superVars.collateralXToken) + .isAtomicToken(params.collateralTokenId) + ) { + uint256 multiplier = IAtomicCollateralizableERC721( + superVars.collateralXToken + ).getTraitMultiplier(params.collateralTokenId); + if (multiplier == 0) { + vars.collateralPrice = IPriceOracleGetter(params.priceOracle) + .getTokenPrice( + params.collateralAsset, + params.collateralTokenId + ); + } else { + vars.collateralPrice = IPriceOracleGetter(params.priceOracle) + .getAssetPrice(params.collateralAsset) + .wadMul(multiplier); + } } else { vars.collateralPrice = IPriceOracleGetter(params.priceOracle) .getAssetPrice(params.collateralAsset); diff --git a/contracts/protocol/tokenization/NToken.sol b/contracts/protocol/tokenization/NToken.sol index 72ffaa911..83f6274ec 100644 --- a/contracts/protocol/tokenization/NToken.sol +++ b/contracts/protocol/tokenization/NToken.sol @@ -40,13 +40,8 @@ contract NToken is VersionedInitializable, MintableIncentivizedERC721, INToken { * @dev Constructor. * @param pool The address of the Pool contract */ - constructor(IPool pool, bool atomic_pricing) - MintableIncentivizedERC721( - pool, - "NTOKEN_IMPL", - "NTOKEN_IMPL", - atomic_pricing - ) + constructor(IPool pool) + MintableIncentivizedERC721(pool, "NTOKEN_IMPL", "NTOKEN_IMPL") {} function initialize( @@ -321,10 +316,6 @@ contract NToken is VersionedInitializable, MintableIncentivizedERC721, INToken { return IERC721Metadata(_underlyingAsset).tokenURI(tokenId); } - function getAtomicPricingConfig() external view returns (bool) { - return ATOMIC_PRICING; - } - function getXTokenType() external pure diff --git a/contracts/protocol/tokenization/NTokenApeStaking.sol b/contracts/protocol/tokenization/NTokenApeStaking.sol index 37ed58c8c..db0d492b1 100644 --- a/contracts/protocol/tokenization/NTokenApeStaking.sol +++ b/contracts/protocol/tokenization/NTokenApeStaking.sol @@ -36,7 +36,7 @@ abstract contract NTokenApeStaking is NToken, INTokenApeStaking { * @dev Constructor. * @param pool The address of the Pool contract */ - constructor(IPool pool, address apeCoinStaking) NToken(pool, false) { + constructor(IPool pool, address apeCoinStaking) NToken(pool) { _apeCoinStaking = ApeCoinStaking(apeCoinStaking); } diff --git a/contracts/protocol/tokenization/NTokenBAKC.sol b/contracts/protocol/tokenization/NTokenBAKC.sol index d83c191ed..89e5ee162 100644 --- a/contracts/protocol/tokenization/NTokenBAKC.sol +++ b/contracts/protocol/tokenization/NTokenBAKC.sol @@ -35,7 +35,7 @@ contract NTokenBAKC is NToken { address apeCoinStaking, address _nBAYC, address _nMAYC - ) NToken(pool, false) { + ) NToken(pool) { _apeCoinStaking = ApeCoinStaking(apeCoinStaking); nBAYC = _nBAYC; nMAYC = _nMAYC; diff --git a/contracts/protocol/tokenization/NTokenMoonBirds.sol b/contracts/protocol/tokenization/NTokenMoonBirds.sol index 52ca79710..66439426f 100644 --- a/contracts/protocol/tokenization/NTokenMoonBirds.sol +++ b/contracts/protocol/tokenization/NTokenMoonBirds.sol @@ -29,7 +29,7 @@ contract NTokenMoonBirds is NToken, IMoonBirdBase { * @dev Constructor. * @param pool The address of the Pool contract */ - constructor(IPool pool) NToken(pool, false) { + constructor(IPool pool) NToken(pool) { // Intentionally left blank } diff --git a/contracts/protocol/tokenization/NTokenUniswapV3.sol b/contracts/protocol/tokenization/NTokenUniswapV3.sol index cd4f52dad..9590bf722 100644 --- a/contracts/protocol/tokenization/NTokenUniswapV3.sol +++ b/contracts/protocol/tokenization/NTokenUniswapV3.sol @@ -30,7 +30,7 @@ contract NTokenUniswapV3 is NToken, INTokenUniswapV3 { * @dev Constructor. * @param pool The address of the Pool contract */ - constructor(IPool pool) NToken(pool, true) { + constructor(IPool pool) NToken(pool) { _ERC721Data.balanceLimit = 30; } @@ -141,6 +141,19 @@ contract NTokenUniswapV3 is NToken, INTokenUniswapV3 { ); } + function isAtomicToken(uint256) external pure override returns (bool) { + return true; + } + + function setTraitsMultipliers(uint256[] calldata, uint256[] calldata) + external + override + onlyPoolAdmin + nonReentrant + { + revert(); + } + function _safeTransferETH(address to, uint256 value) internal { (bool success, ) = to.call{value: value}(new bytes(0)); require(success, "ETH_TRANSFER_FAILED"); diff --git a/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol b/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol index ee269c2b0..2f28fc07a 100644 --- a/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol +++ b/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol @@ -10,6 +10,7 @@ import {IERC721} from "../../../dependencies/openzeppelin/contracts/IERC721.sol" import {IERC721Receiver} from "../../../dependencies/openzeppelin/contracts/IERC721Receiver.sol"; import {IERC721Enumerable} from "../../../dependencies/openzeppelin/contracts/IERC721Enumerable.sol"; import {ICollateralizableERC721} from "../../../interfaces/ICollateralizableERC721.sol"; +import {IAtomicCollateralizableERC721} from "../../../interfaces/IAtomicCollateralizableERC721.sol"; import {IAuctionableERC721} from "../../../interfaces/IAuctionableERC721.sol"; import {SafeCast} from "../../../dependencies/openzeppelin/contracts/SafeCast.sol"; import {WadRayMath} from "../../libraries/math/WadRayMath.sol"; @@ -30,6 +31,7 @@ import {MintableERC721Logic, UserState, MintableERC721Data} from "../libraries/M abstract contract MintableIncentivizedERC721 is ReentrancyGuard, ICollateralizableERC721, + IAtomicCollateralizableERC721, IAuctionableERC721, Context, IERC721Metadata, @@ -71,7 +73,6 @@ abstract contract MintableIncentivizedERC721 is IPoolAddressesProvider internal immutable _addressesProvider; IPool public immutable POOL; - bool public immutable ATOMIC_PRICING; address internal _underlyingAsset; @@ -84,14 +85,12 @@ abstract contract MintableIncentivizedERC721 is constructor( IPool pool, string memory name_, - string memory symbol_, - bool atomic_pricing + string memory symbol_ ) { _addressesProvider = pool.ADDRESSES_PROVIDER(); _ERC721Data.name = name_; _ERC721Data.symbol = symbol_; POOL = pool; - ATOMIC_PRICING = atomic_pricing; } function name() public view override returns (string memory) { @@ -109,7 +108,9 @@ abstract contract MintableIncentivizedERC721 is override returns (uint256) { - return _ERC721Data.userState[account].balance; + return + _ERC721Data.userState[account].balance + + _ERC721Data.userState[account].atomicBalance; } /** @@ -374,12 +375,7 @@ abstract contract MintableIncentivizedERC721 is ) { return - MintableERC721Logic.executeMintMultiple( - _ERC721Data, - ATOMIC_PRICING, - to, - tokenData - ); + MintableERC721Logic.executeMintMultiple(_ERC721Data, to, tokenData); } function _burnMultiple(address user, uint256[] calldata tokenIds) @@ -418,7 +414,6 @@ abstract contract MintableIncentivizedERC721 is MintableERC721Logic.executeTransfer( _ERC721Data, POOL, - ATOMIC_PRICING, from, to, tokenId @@ -437,7 +432,6 @@ abstract contract MintableIncentivizedERC721 is .executeTransferCollateralizable( _ERC721Data, POOL, - ATOMIC_PRICING, from, to, tokenId @@ -452,7 +446,31 @@ abstract contract MintableIncentivizedERC721 is override returns (uint256) { - return _ERC721Data.userState[account].collateralizedBalance; + return + _ERC721Data.userState[account].collateralizedBalance + + _ERC721Data.userState[account].atomicCollateralizedBalance; + } + + /// @inheritdoc IAtomicCollateralizableERC721 + function atomicCollateralizedBalanceOf(address account) + public + view + virtual + override + returns (uint256) + { + return _ERC721Data.userState[account].atomicCollateralizedBalance; + } + + /// @inheritdoc IAtomicCollateralizableERC721 + function atomicBalanceOf(address account) + public + view + virtual + override + returns (uint256) + { + return _ERC721Data.userState[account].atomicBalance; } /// @inheritdoc ICollateralizableERC721 @@ -487,9 +505,9 @@ abstract contract MintableIncentivizedERC721 is uint256 newCollateralizedBalance ) { - oldCollateralizedBalance = _ERC721Data - .userState[sender] - .collateralizedBalance; + oldCollateralizedBalance = + _ERC721Data.userState[sender].collateralizedBalance + + _ERC721Data.userState[sender].atomicCollateralizedBalance; for (uint256 index = 0; index < tokenIds.length; index++) { MintableERC721Logic.executeSetIsUsedAsCollateral( @@ -501,9 +519,9 @@ abstract contract MintableIncentivizedERC721 is ); } - newCollateralizedBalance = _ERC721Data - .userState[sender] - .collateralizedBalance; + newCollateralizedBalance = + _ERC721Data.userState[sender].collateralizedBalance + + _ERC721Data.userState[sender].atomicCollateralizedBalance; } /// @inheritdoc ICollateralizableERC721 @@ -526,6 +544,25 @@ abstract contract MintableIncentivizedERC721 is return MintableERC721Logic.isAuctioned(_ERC721Data, POOL, tokenId); } + /// @inheritdoc IAtomicCollateralizableERC721 + function isAtomicToken(uint256 tokenId) + external + view + virtual + returns (bool) + { + return MintableERC721Logic.isAtomicToken(_ERC721Data, tokenId); + } + + /// @inheritdoc IAtomicCollateralizableERC721 + function getTraitMultiplier(uint256 tokenId) + external + view + returns (uint256) + { + return _ERC721Data.traitsMultipliers[tokenId]; + } + /// @inheritdoc IAuctionableERC721 function startAuction(uint256 tokenId) external @@ -548,6 +585,17 @@ abstract contract MintableIncentivizedERC721 is MintableERC721Logic.executeEndAuction(_ERC721Data, POOL, tokenId); } + function setTraitsMultipliers( + uint256[] calldata tokenIds, + uint256[] calldata multipliers + ) external virtual onlyPoolAdmin nonReentrant { + MintableERC721Logic.executeSetTraitsMultipliers( + _ERC721Data, + tokenIds, + multipliers + ); + } + /// @inheritdoc IAuctionableERC721 function getAuctionData(uint256 tokenId) external @@ -594,11 +642,17 @@ abstract contract MintableIncentivizedERC721 is override returns (uint256) { + uint256 balance = _ERC721Data.userState[owner].balance; + uint256 atomicBalance = _ERC721Data.userState[owner].atomicBalance; require( - index < balanceOf(owner), + index < balance + atomicBalance, "ERC721Enumerable: owner index out of bounds" ); - return _ERC721Data.ownedTokens[owner][index]; + if (index < balance) { + return _ERC721Data.ownedTokens[owner][index]; + } else { + return _ERC721Data.ownedAtomicTokens[owner][index - balance]; + } } /** diff --git a/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol b/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol index e8345fa4a..bc60feb48 100644 --- a/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol +++ b/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.10; import {ApeCoinStaking} from "../../../dependencies/yoga-labs/ApeCoinStaking.sol"; import {IERC721} from "../../../dependencies/openzeppelin/contracts/IERC721.sol"; import {SafeERC20} from "../../../dependencies/openzeppelin/contracts/SafeERC20.sol"; +import {WadRayMath} from "../../libraries/math/WadRayMath.sol"; import {IERC20} from "../../../dependencies/openzeppelin/contracts/IERC20.sol"; import "../../../interfaces/IRewardController.sol"; import "../../libraries/types/DataTypes.sol"; @@ -13,6 +14,8 @@ struct UserState { uint64 balance; uint64 collateralizedBalance; uint128 additionalData; + uint64 atomicBalance; + uint64 atomicCollateralizedBalance; } struct MintableERC721Data { @@ -42,6 +45,21 @@ struct MintableERC721Data { uint64 balanceLimit; mapping(uint256 => bool) isUsedAsCollateral; mapping(uint256 => DataTypes.Auction) auctions; + // Mapping from owner to list of owned token IDs + mapping(address => mapping(uint256 => uint256)) ownedAtomicTokens; + // Mapping from token ID to index of the owner tokens list + mapping(uint256 => uint256) ownedAtomicTokensIndex; + // All atomic tokens' traits multipliers + mapping(uint256 => uint256) traitsMultipliers; +} + +struct LocalVars { + uint64 oldBalance; + uint64 oldAtomicBalance; + uint64 oldCollateralizedBalance; + uint64 oldAtomicCollateralizedBalance; + uint64 collateralizedTokens; + uint64 collateralizedAtomicTokens; } /** @@ -50,6 +68,17 @@ struct MintableERC721Data { * @notice Implements the base logic for MintableERC721 */ library MintableERC721Logic { + /** + * @dev This constant represents the maximum trait multiplier that a single tokenId can have + * A value of 10e18 results in 10x of price + */ + uint256 internal constant MAX_TRAIT_MULTIPLIER = 10e18; + /** + * @dev This constant represents the minimum trait multiplier that a single tokenId can have + * A value of 1e18 results in no price multiplier + */ + uint256 internal constant MIN_TRAIT_MULTIPLIER = 0.5e18; + /** * @dev Emitted when `tokenId` token is transferred from `from` to `to`. */ @@ -80,7 +109,6 @@ library MintableERC721Logic { function executeTransfer( MintableERC721Data storage erc721Data, IPool POOL, - bool ATOMIC_PRICING, address from, address to, uint256 tokenId @@ -94,18 +122,34 @@ library MintableERC721Logic { !isAuctioned(erc721Data, POOL, tokenId), Errors.TOKEN_IN_AUCTION ); - - _beforeTokenTransfer(erc721Data, from, to, tokenId); + bool isAtomic = isAtomicToken(erc721Data, tokenId); + _beforeTokenTransfer(erc721Data, from, to, tokenId, isAtomic); // Clear approvals from the previous owner _approve(erc721Data, address(0), tokenId); uint64 oldSenderBalance = erc721Data.userState[from].balance; - erc721Data.userState[from].balance = oldSenderBalance - 1; + uint64 oldSenderAtomicBalance = erc721Data + .userState[from] + .atomicBalance; + if (isAtomic) { + erc721Data.userState[from].atomicBalance = + oldSenderAtomicBalance - + 1; + } else { + erc721Data.userState[from].balance = oldSenderBalance - 1; + } uint64 oldRecipientBalance = erc721Data.userState[to].balance; - uint64 newRecipientBalance = oldRecipientBalance + 1; - _checkBalanceLimit(erc721Data, ATOMIC_PRICING, newRecipientBalance); - erc721Data.userState[to].balance = newRecipientBalance; + uint64 oldRecipientAtomicBalance = erc721Data + .userState[to] + .atomicBalance; + if (isAtomic) { + uint64 newRecipientAtomicBalance = oldRecipientAtomicBalance + 1; + _checkAtomicBalanceLimit(erc721Data, newRecipientAtomicBalance); + erc721Data.userState[to].atomicBalance = newRecipientAtomicBalance; + } else { + erc721Data.userState[to].balance = oldRecipientBalance + 1; + } erc721Data.owners[tokenId] = to; if (from != to && erc721Data.auctions[tokenId].startTime > 0) { @@ -118,13 +162,13 @@ library MintableERC721Logic { rewardControllerLocal.handleAction( from, oldTotalSupply, - oldSenderBalance + oldSenderBalance + oldSenderAtomicBalance ); if (from != to) { rewardControllerLocal.handleAction( to, oldTotalSupply, - oldRecipientBalance + oldRecipientBalance + oldRecipientAtomicBalance ); } } @@ -135,7 +179,6 @@ library MintableERC721Logic { function executeTransferCollateralizable( MintableERC721Data storage erc721Data, IPool POOL, - bool ATOMIC_PRICING, address from, address to, uint256 tokenId @@ -143,11 +186,15 @@ library MintableERC721Logic { isUsedAsCollateral_ = erc721Data.isUsedAsCollateral[tokenId]; if (from != to && isUsedAsCollateral_) { - erc721Data.userState[from].collateralizedBalance -= 1; + if (isAtomicToken(erc721Data, tokenId)) { + erc721Data.userState[from].atomicCollateralizedBalance -= 1; + } else { + erc721Data.userState[from].collateralizedBalance -= 1; + } delete erc721Data.isUsedAsCollateral[tokenId]; } - executeTransfer(erc721Data, POOL, ATOMIC_PRICING, from, to, tokenId); + executeTransfer(erc721Data, POOL, from, to, tokenId); } function executeSetIsUsedAsCollateral( @@ -170,39 +217,47 @@ library MintableERC721Logic { ); } - uint64 collateralizedBalance = erc721Data - .userState[owner] - .collateralizedBalance; - erc721Data.isUsedAsCollateral[tokenId] = useAsCollateral; - collateralizedBalance = useAsCollateral - ? collateralizedBalance + 1 - : collateralizedBalance - 1; - erc721Data - .userState[owner] - .collateralizedBalance = collateralizedBalance; + if (isAtomicToken(erc721Data, tokenId)) { + uint64 collateralizedBalance = erc721Data + .userState[owner] + .atomicCollateralizedBalance; + erc721Data.isUsedAsCollateral[tokenId] = useAsCollateral; + collateralizedBalance = useAsCollateral + ? collateralizedBalance + 1 + : collateralizedBalance - 1; + erc721Data + .userState[owner] + .atomicCollateralizedBalance = collateralizedBalance; + } else { + uint64 collateralizedBalance = erc721Data + .userState[owner] + .collateralizedBalance; + erc721Data.isUsedAsCollateral[tokenId] = useAsCollateral; + collateralizedBalance = useAsCollateral + ? collateralizedBalance + 1 + : collateralizedBalance - 1; + erc721Data + .userState[owner] + .collateralizedBalance = collateralizedBalance; + } return true; } function executeMintMultiple( MintableERC721Data storage erc721Data, - bool ATOMIC_PRICING, address to, DataTypes.ERC721SupplyParams[] calldata tokenData ) external returns ( - uint64 oldCollateralizedBalance, - uint64 newCollateralizedBalance + uint64 oldTotalCollateralizedBalance, + uint64 newTotalCollateralizedBalance ) { require(to != address(0), "ERC721: mint to the zero address"); - uint64 oldBalance = erc721Data.userState[to].balance; - oldCollateralizedBalance = erc721Data - .userState[to] - .collateralizedBalance; + LocalVars memory vars = _cache(erc721Data, to); uint256 oldTotalSupply = erc721Data.allTokens.length; - uint64 collateralizedTokens = 0; for (uint256 index = 0; index < tokenData.length; index++) { uint256 tokenId = tokenData[index].tokenId; @@ -217,12 +272,19 @@ library MintableERC721Logic { tokenId, oldTotalSupply + index ); + bool isAtomic = isAtomicToken(erc721Data, tokenId); _addTokenToOwnerEnumeration( erc721Data, to, tokenId, - oldBalance + index + isAtomic ? vars.oldAtomicBalance : vars.oldBalance, + isAtomic ); + if (isAtomic) { + vars.oldAtomicBalance += 1; + } else { + vars.oldBalance += 1; + } erc721Data.owners[tokenId] = to; @@ -231,30 +293,46 @@ library MintableERC721Logic { !erc721Data.isUsedAsCollateral[tokenId] ) { erc721Data.isUsedAsCollateral[tokenId] = true; - collateralizedTokens++; + if (isAtomic) { + vars.collateralizedAtomicTokens++; + } else { + vars.collateralizedTokens++; + } } emit Transfer(address(0), to, tokenId); } - newCollateralizedBalance = - oldCollateralizedBalance + - collateralizedTokens; + uint64 newCollateralizedBalance = vars.oldCollateralizedBalance + + vars.collateralizedTokens; + uint64 newAtomicCollateralizedBalance = vars + .oldAtomicCollateralizedBalance + vars.collateralizedAtomicTokens; erc721Data .userState[to] .collateralizedBalance = newCollateralizedBalance; + erc721Data + .userState[to] + .atomicCollateralizedBalance = newAtomicCollateralizedBalance; + + _checkAtomicBalanceLimit(erc721Data, vars.oldAtomicBalance); - uint64 newBalance = oldBalance + uint64(tokenData.length); - _checkBalanceLimit(erc721Data, ATOMIC_PRICING, newBalance); - erc721Data.userState[to].balance = newBalance; + erc721Data.userState[to].balance = vars.oldBalance; + erc721Data.userState[to].atomicBalance = vars.oldAtomicBalance; // calculate incentives IRewardController rewardControllerLocal = erc721Data.rewardController; if (address(rewardControllerLocal) != address(0)) { - rewardControllerLocal.handleAction(to, oldTotalSupply, oldBalance); + rewardControllerLocal.handleAction( + to, + oldTotalSupply, + vars.oldBalance + vars.oldAtomicBalance - tokenData.length + ); } - return (oldCollateralizedBalance, newCollateralizedBalance); + return ( + vars.oldCollateralizedBalance + vars.oldAtomicCollateralizedBalance, + newCollateralizedBalance + newAtomicCollateralizedBalance + ); } function executeBurnMultiple( @@ -269,13 +347,8 @@ library MintableERC721Logic { uint64 newCollateralizedBalance ) { - uint64 burntCollateralizedTokens = 0; - uint64 balanceToBurn; + LocalVars memory vars = _cache(erc721Data, user); uint256 oldTotalSupply = erc721Data.allTokens.length; - uint256 oldBalance = erc721Data.userState[user].balance; - oldCollateralizedBalance = erc721Data - .userState[user] - .collateralizedBalance; for (uint256 index = 0; index < tokenIds.length; index++) { uint256 tokenId = tokenIds[index]; @@ -291,17 +364,23 @@ library MintableERC721Logic { tokenId, oldTotalSupply - index ); + bool isAtomic = isAtomicToken(erc721Data, tokenId); _removeTokenFromOwnerEnumeration( erc721Data, user, tokenId, - oldBalance - index + isAtomic ? vars.oldAtomicBalance : vars.oldBalance, + isAtomic ); + if (isAtomic) { + vars.oldAtomicBalance -= 1; + } else { + vars.oldBalance -= 1; + } // Clear approvals _approve(erc721Data, address(0), tokenId); - balanceToBurn++; delete erc721Data.owners[tokenId]; if (erc721Data.auctions[tokenId].startTime > 0) { @@ -310,18 +389,28 @@ library MintableERC721Logic { if (erc721Data.isUsedAsCollateral[tokenId]) { delete erc721Data.isUsedAsCollateral[tokenId]; - burntCollateralizedTokens++; + if (isAtomic) { + vars.collateralizedAtomicTokens += 1; + } else { + vars.collateralizedTokens += 1; + } } emit Transfer(owner, address(0), tokenId); } - erc721Data.userState[user].balance -= balanceToBurn; - newCollateralizedBalance = - oldCollateralizedBalance - - burntCollateralizedTokens; + erc721Data.userState[user].balance = vars.oldBalance; + erc721Data.userState[user].atomicBalance = vars.oldAtomicBalance; + + uint64 newCollateralizedBalance = vars.oldCollateralizedBalance - + vars.collateralizedTokens; + uint64 newAtomicCollateralizedBalance = vars + .oldAtomicCollateralizedBalance - vars.collateralizedAtomicTokens; erc721Data .userState[user] .collateralizedBalance = newCollateralizedBalance; + erc721Data + .userState[user] + .atomicCollateralizedBalance = newAtomicCollateralizedBalance; // calculate incentives IRewardController rewardControllerLocal = erc721Data.rewardController; @@ -330,11 +419,14 @@ library MintableERC721Logic { rewardControllerLocal.handleAction( user, oldTotalSupply, - oldBalance + vars.oldBalance + vars.oldAtomicBalance - tokenIds.length ); } - return (oldCollateralizedBalance, newCollateralizedBalance); + return ( + vars.oldCollateralizedBalance + vars.oldAtomicCollateralizedBalance, + newCollateralizedBalance + newAtomicCollateralizedBalance + ); } function executeApprove( @@ -399,20 +491,101 @@ library MintableERC721Logic { delete erc721Data.auctions[tokenId]; } - function _checkBalanceLimit( + function executeSetTraitsMultipliers( MintableERC721Data storage erc721Data, - bool ATOMIC_PRICING, - uint64 balance - ) private view { - if (ATOMIC_PRICING) { - uint64 balanceLimit = erc721Data.balanceLimit; - require( - balanceLimit == 0 || balance <= balanceLimit, - Errors.NTOKEN_BALANCE_EXCEEDED - ); + uint256[] calldata tokenIds, + uint256[] calldata multipliers + ) external { + require( + tokenIds.length == multipliers.length, + Errors.INCONSISTENT_PARAMS_LENGTH + ); + for (uint256 i = 0; i < tokenIds.length; i++) { + _checkTraitMultiplier(multipliers[i]); + address owner = erc721Data.owners[tokenIds[i]]; + uint256 oldMultiplier = erc721Data.traitsMultipliers[tokenIds[i]]; + erc721Data.traitsMultipliers[tokenIds[i]] = multipliers[i]; + if (owner == address(0)) { + continue; + } + + bool isAtomicPrev = oldMultiplier != 0 && + oldMultiplier != WadRayMath.WAD; + bool isAtomicNext = multipliers[i] != WadRayMath.WAD; + + if (isAtomicPrev && !isAtomicNext) { + _removeTokenFromOwnerEnumeration( + erc721Data, + owner, + tokenIds[i], + erc721Data.userState[owner].atomicBalance, + isAtomicPrev + ); + _addTokenToOwnerEnumeration( + erc721Data, + owner, + tokenIds[i], + erc721Data.userState[owner].balance, + isAtomicNext + ); + + erc721Data.userState[owner].atomicBalance -= 1; + erc721Data.userState[owner].balance += 1; + + if (erc721Data.isUsedAsCollateral[tokenIds[i]]) { + erc721Data + .userState[owner] + .atomicCollateralizedBalance -= 1; + erc721Data.userState[owner].collateralizedBalance += 1; + } + } else if (!isAtomicPrev && isAtomicNext) { + _removeTokenFromOwnerEnumeration( + erc721Data, + owner, + tokenIds[i], + erc721Data.userState[owner].balance, + isAtomicPrev + ); + _addTokenToOwnerEnumeration( + erc721Data, + owner, + tokenIds[i], + erc721Data.userState[owner].atomicBalance, + isAtomicNext + ); + + erc721Data.userState[owner].balance -= 1; + erc721Data.userState[owner].atomicBalance += 1; + + if (erc721Data.isUsedAsCollateral[tokenIds[i]]) { + erc721Data.userState[owner].collateralizedBalance -= 1; + erc721Data + .userState[owner] + .atomicCollateralizedBalance += 1; + } + } } } + function _checkAtomicBalanceLimit( + MintableERC721Data storage erc721Data, + uint64 atomicBalance + ) private view { + uint64 balanceLimit = erc721Data.balanceLimit; + require( + balanceLimit == 0 || atomicBalance <= balanceLimit, + Errors.NTOKEN_BALANCE_EXCEEDED + ); + } + + function _checkTraitMultiplier(uint256 multiplier) private pure { + require( + multiplier > MIN_TRAIT_MULTIPLIER && + multiplier < MAX_TRAIT_MULTIPLIER, + Errors.INVALID_AMOUNT + ); + } + function _exists(MintableERC721Data storage erc721Data, uint256 tokenId) private view @@ -421,6 +594,29 @@ library MintableERC721Logic { return erc721Data.owners[tokenId] != address(0); } + function _cache(MintableERC721Data storage erc721Data, address user) + private + view + returns (LocalVars memory vars) + { + vars.oldBalance = erc721Data.userState[user].balance; + vars.oldAtomicBalance = erc721Data.userState[user].atomicBalance; + vars.oldCollateralizedBalance = erc721Data + .userState[user] + .collateralizedBalance; + vars.oldAtomicCollateralizedBalance = erc721Data + .userState[user] + .atomicCollateralizedBalance; + } + + function isAtomicToken( + MintableERC721Data storage erc721Data, + uint256 tokenId + ) public view returns (bool) { + uint256 multiplier = erc721Data.traitsMultipliers[tokenId]; + return multiplier != 0 && multiplier != WadRayMath.WAD; + } + function isAuctioned( MintableERC721Data storage erc721Data, IPool POOL, @@ -452,26 +648,38 @@ library MintableERC721Logic { MintableERC721Data storage erc721Data, address from, address to, - uint256 tokenId + uint256 tokenId, + bool isAtomic ) private { if (from == address(0)) { uint256 length = erc721Data.allTokens.length; _addTokenToAllTokensEnumeration(erc721Data, tokenId, length); } else if (from != to) { - uint256 userBalance = erc721Data.userState[from].balance; + uint256 userBalance = isAtomic + ? erc721Data.userState[from].atomicBalance + : erc721Data.userState[from].balance; _removeTokenFromOwnerEnumeration( erc721Data, from, tokenId, - userBalance + userBalance, + isAtomic ); } if (to == address(0)) { uint256 length = erc721Data.allTokens.length; _removeTokenFromAllTokensEnumeration(erc721Data, tokenId, length); } else if (to != from) { - uint256 length = erc721Data.userState[to].balance; - _addTokenToOwnerEnumeration(erc721Data, to, tokenId, length); + uint256 length = isAtomic + ? erc721Data.userState[to].atomicBalance + : erc721Data.userState[to].balance; + _addTokenToOwnerEnumeration( + erc721Data, + to, + tokenId, + length, + isAtomic + ); } } @@ -479,15 +687,22 @@ library MintableERC721Logic { * @dev Private function to add a token to this extension's ownership-tracking data structures. * @param to address representing the new owner of the given token ID * @param tokenId uint256 ID of the token to be added to the tokens list of the given address + * @param isAtomic whether it's an atomic token */ function _addTokenToOwnerEnumeration( MintableERC721Data storage erc721Data, address to, uint256 tokenId, - uint256 length + uint256 length, + bool isAtomic ) private { - erc721Data.ownedTokens[to][length] = tokenId; - erc721Data.ownedTokensIndex[tokenId] = length; + if (isAtomic) { + erc721Data.ownedAtomicTokens[to][length] = tokenId; + erc721Data.ownedAtomicTokensIndex[tokenId] = length; + } else { + erc721Data.ownedTokens[to][length] = tokenId; + erc721Data.ownedTokensIndex[tokenId] = length; + } } /** @@ -510,30 +725,53 @@ library MintableERC721Logic { * This has O(1) time complexity, but alters the order of the _ownedTokens array. * @param from address representing the previous owner of the given token ID * @param tokenId uint256 ID of the token to be removed from the tokens list of the given address + * @param isAtomic whether it's an atomic token */ function _removeTokenFromOwnerEnumeration( MintableERC721Data storage erc721Data, address from, uint256 tokenId, - uint256 userBalance + uint256 userBalance, + bool isAtomic ) private { // To prevent a gap in from's tokens array, we store the last token in the index of the token to delete, and // then delete the last slot (swap and pop). - uint256 lastTokenIndex = userBalance - 1; - uint256 tokenIndex = erc721Data.ownedTokensIndex[tokenId]; + if (isAtomic) { + uint256 lastTokenIndex = userBalance - 1; + uint256 tokenIndex = erc721Data.ownedAtomicTokensIndex[tokenId]; - // When the token to delete is the last token, the swap operation is unnecessary - if (tokenIndex != lastTokenIndex) { - uint256 lastTokenId = erc721Data.ownedTokens[from][lastTokenIndex]; + // When the token to delete is the last token, the swap operation is unnecessary + if (tokenIndex != lastTokenIndex) { + uint256 lastTokenId = erc721Data.ownedAtomicTokens[from][ + lastTokenIndex + ]; - erc721Data.ownedTokens[from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token - erc721Data.ownedTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index - } + erc721Data.ownedAtomicTokens[from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token + erc721Data.ownedAtomicTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index + } - // This also deletes the contents at the last position of the array - delete erc721Data.ownedTokensIndex[tokenId]; - delete erc721Data.ownedTokens[from][lastTokenIndex]; + // This also deletes the contents at the last position of the array + delete erc721Data.ownedAtomicTokensIndex[tokenId]; + delete erc721Data.ownedAtomicTokens[from][lastTokenIndex]; + } else { + uint256 lastTokenIndex = userBalance - 1; + uint256 tokenIndex = erc721Data.ownedTokensIndex[tokenId]; + + // When the token to delete is the last token, the swap operation is unnecessary + if (tokenIndex != lastTokenIndex) { + uint256 lastTokenId = erc721Data.ownedTokens[from][ + lastTokenIndex + ]; + + erc721Data.ownedTokens[from][tokenIndex] = lastTokenId; // Move the last token to the slot of the to-delete token + erc721Data.ownedTokensIndex[lastTokenId] = tokenIndex; // Update the moved token's index + } + + // This also deletes the contents at the last position of the array + delete erc721Data.ownedTokensIndex[tokenId]; + delete erc721Data.ownedTokens[from][lastTokenIndex]; + } } /** diff --git a/contracts/ui/UiPoolDataProvider.sol b/contracts/ui/UiPoolDataProvider.sol index 1241baa31..a352f8ddc 100644 --- a/contracts/ui/UiPoolDataProvider.sol +++ b/contracts/ui/UiPoolDataProvider.sol @@ -10,6 +10,7 @@ import {IPool} from "../interfaces/IPool.sol"; import {IParaSpaceOracle} from "../interfaces/IParaSpaceOracle.sol"; import {IPToken} from "../interfaces/IPToken.sol"; import {ICollateralizableERC721} from "../interfaces/ICollateralizableERC721.sol"; +import {IAtomicCollateralizableERC721} from "../interfaces/IAtomicCollateralizableERC721.sol"; import {IAuctionableERC721} from "../interfaces/IAuctionableERC721.sol"; import {INToken} from "../interfaces/INToken.sol"; import {IVariableDebtToken} from "../interfaces/IVariableDebtToken.sol"; @@ -163,7 +164,6 @@ contract UiPoolDataProvider is IUiPoolDataProvider { ).name(); } - reserveData.isAtomicPricing = false; if (reserveData.underlyingAsset != SAPE_ADDRESS) { reserveData.availableLiquidity = IERC20Detailed( reserveData.underlyingAsset @@ -179,8 +179,6 @@ contract UiPoolDataProvider is IUiPoolDataProvider { reserveData.availableLiquidity = IERC721( reserveData.underlyingAsset ).balanceOf(reserveData.xTokenAddress); - reserveData.isAtomicPricing = INToken(reserveData.xTokenAddress) - .getAtomicPricingConfig(); } ( @@ -494,12 +492,25 @@ contract UiPoolDataProvider is IUiPoolDataProvider { tokenData.tokenId ); // token price - if (INToken(baseData.xTokenAddress).getAtomicPricingConfig()) { - try - oracle.getTokenPrice(tokenData.asset, tokenData.tokenId) - returns (uint256 price) { - tokenData.tokenPrice = price; - } catch {} + if ( + IAtomicCollateralizableERC721(baseData.xTokenAddress) + .isAtomicToken(tokenData.tokenId) + ) { + uint256 multiplier = IAtomicCollateralizableERC721( + baseData.xTokenAddress + ).getTraitMultiplier(tokenData.tokenId); + if (multiplier == 0) { + try + oracle.getTokenPrice( + tokenData.asset, + tokenData.tokenId + ) + returns (uint256 price) { + tokenData.tokenPrice = price.wadMul(multiplier); + } catch {} + } else { + tokenData.tokenPrice = collectionPrice; + } } else { tokenData.tokenPrice = collectionPrice; } diff --git a/contracts/ui/interfaces/IUiPoolDataProvider.sol b/contracts/ui/interfaces/IUiPoolDataProvider.sol index 9947e260e..54cfa103a 100644 --- a/contracts/ui/interfaces/IUiPoolDataProvider.sol +++ b/contracts/ui/interfaces/IUiPoolDataProvider.sol @@ -27,7 +27,6 @@ interface IUiPoolDataProvider { bool isActive; bool isFrozen; bool isPaused; - bool isAtomicPricing; // base data uint128 liquidityIndex; uint128 variableBorrowIndex; From 5f10fb598617b80cb0617614e7e4f57d93f7dbc6 Mon Sep 17 00:00:00 2001 From: GopherJ Date: Tue, 31 Jan 2023 23:08:53 +0800 Subject: [PATCH 02/19] fix: fetching correct price Signed-off-by: GopherJ --- .../protocol/libraries/logic/GenericLogic.sol | 25 ++++++++++++++----- .../libraries/logic/LiquidationLogic.sol | 24 ++++++++++-------- .../protocol/tokenization/NTokenUniswapV3.sol | 4 --- contracts/ui/UiPoolDataProvider.sol | 23 ++++++++--------- 4 files changed, 43 insertions(+), 33 deletions(-) diff --git a/contracts/protocol/libraries/logic/GenericLogic.sol b/contracts/protocol/libraries/logic/GenericLogic.sol index 20f94e06a..0c0e7d666 100644 --- a/contracts/protocol/libraries/logic/GenericLogic.sol +++ b/contracts/protocol/libraries/logic/GenericLogic.sol @@ -16,7 +16,7 @@ import {WadRayMath} from "../math/WadRayMath.sol"; import {DataTypes} from "../types/DataTypes.sol"; import {ReserveLogic} from "./ReserveLogic.sol"; import {INonfungiblePositionManager} from "../../../dependencies/uniswap/INonfungiblePositionManager.sol"; -import {XTokenType} from "../../../interfaces/IXTokenType.sol"; +import {XTokenType, IXTokenType} from "../../../interfaces/IXTokenType.sol"; /** * @title GenericLogic library @@ -390,11 +390,24 @@ library GenericLogic { tokenId ) ) { - totalValue += _getTokenPrice( - params.oracle, - vars.currentReserveAddress, - tokenId - ); + if ( + IXTokenType(vars.xTokenAddress).getXTokenType() == + XTokenType.NTokenUniswapV3 + ) { + totalValue += _getTokenPrice( + params.oracle, + vars.currentReserveAddress, + tokenId + ); + } else if ( + IAtomicCollateralizableERC721(vars.xTokenAddress) + .isAtomicToken(tokenId) + ) { + uint256 multiplier = IAtomicCollateralizableERC721( + vars.xTokenAddress + ).getTraitMultiplier(tokenId); + totalValue += assetPrice.wadMul(multiplier); + } } } } diff --git a/contracts/protocol/libraries/logic/LiquidationLogic.sol b/contracts/protocol/libraries/logic/LiquidationLogic.sol index 2ec0d22a3..bc344feef 100644 --- a/contracts/protocol/libraries/logic/LiquidationLogic.sol +++ b/contracts/protocol/libraries/logic/LiquidationLogic.sol @@ -20,6 +20,7 @@ import {IWETH} from "../../../misc/interfaces/IWETH.sol"; import {ICollateralizableERC721} from "../../../interfaces/ICollateralizableERC721.sol"; import {IAtomicCollateralizableERC721} from "../../../interfaces/IAtomicCollateralizableERC721.sol"; import {IAuctionableERC721} from "../../../interfaces/IAuctionableERC721.sol"; +import {IXTokenType, XTokenType} from "../../../interfaces/IXTokenType.sol"; import {INToken} from "../../../interfaces/INToken.sol"; import {PRBMath} from "../../../dependencies/math/PRBMath.sol"; import {PRBMathUD60x18} from "../../../dependencies/math/PRBMathUD60x18.sol"; @@ -779,23 +780,24 @@ library LiquidationLogic { // price of the asset that is used as collateral if ( + IXTokenType(superVars.collateralXToken).getXTokenType() == + XTokenType.NTokenUniswapV3 + ) { + vars.collateralPrice = IPriceOracleGetter(params.priceOracle) + .getTokenPrice( + params.collateralAsset, + params.collateralTokenId + ); + } else if ( IAtomicCollateralizableERC721(superVars.collateralXToken) .isAtomicToken(params.collateralTokenId) ) { uint256 multiplier = IAtomicCollateralizableERC721( superVars.collateralXToken ).getTraitMultiplier(params.collateralTokenId); - if (multiplier == 0) { - vars.collateralPrice = IPriceOracleGetter(params.priceOracle) - .getTokenPrice( - params.collateralAsset, - params.collateralTokenId - ); - } else { - vars.collateralPrice = IPriceOracleGetter(params.priceOracle) - .getAssetPrice(params.collateralAsset) - .wadMul(multiplier); - } + vars.collateralPrice = IPriceOracleGetter(params.priceOracle) + .getAssetPrice(params.collateralAsset) + .wadMul(multiplier); } else { vars.collateralPrice = IPriceOracleGetter(params.priceOracle) .getAssetPrice(params.collateralAsset); diff --git a/contracts/protocol/tokenization/NTokenUniswapV3.sol b/contracts/protocol/tokenization/NTokenUniswapV3.sol index 9590bf722..6b3451f50 100644 --- a/contracts/protocol/tokenization/NTokenUniswapV3.sol +++ b/contracts/protocol/tokenization/NTokenUniswapV3.sol @@ -141,10 +141,6 @@ contract NTokenUniswapV3 is NToken, INTokenUniswapV3 { ); } - function isAtomicToken(uint256) external pure override returns (bool) { - return true; - } - function setTraitsMultipliers(uint256[] calldata, uint256[] calldata) external override diff --git a/contracts/ui/UiPoolDataProvider.sol b/contracts/ui/UiPoolDataProvider.sol index a352f8ddc..23139c304 100644 --- a/contracts/ui/UiPoolDataProvider.sol +++ b/contracts/ui/UiPoolDataProvider.sol @@ -11,6 +11,7 @@ import {IParaSpaceOracle} from "../interfaces/IParaSpaceOracle.sol"; import {IPToken} from "../interfaces/IPToken.sol"; import {ICollateralizableERC721} from "../interfaces/ICollateralizableERC721.sol"; import {IAtomicCollateralizableERC721} from "../interfaces/IAtomicCollateralizableERC721.sol"; +import {XTokenType, IXTokenType} from "../interfaces/IXTokenType.sol"; import {IAuctionableERC721} from "../interfaces/IAuctionableERC721.sol"; import {INToken} from "../interfaces/INToken.sol"; import {IVariableDebtToken} from "../interfaces/IVariableDebtToken.sol"; @@ -493,24 +494,22 @@ contract UiPoolDataProvider is IUiPoolDataProvider { ); // token price if ( + IXTokenType(baseData.xTokenAddress).getXTokenType() == + XTokenType.NTokenUniswapV3 + ) { + try + oracle.getTokenPrice(tokenData.asset, tokenData.tokenId) + returns (uint256 price) { + tokenData.tokenPrice = price; + } catch {} + } else if ( IAtomicCollateralizableERC721(baseData.xTokenAddress) .isAtomicToken(tokenData.tokenId) ) { uint256 multiplier = IAtomicCollateralizableERC721( baseData.xTokenAddress ).getTraitMultiplier(tokenData.tokenId); - if (multiplier == 0) { - try - oracle.getTokenPrice( - tokenData.asset, - tokenData.tokenId - ) - returns (uint256 price) { - tokenData.tokenPrice = price.wadMul(multiplier); - } catch {} - } else { - tokenData.tokenPrice = collectionPrice; - } + tokenData.tokenPrice = collectionPrice.wadMul(multiplier); } else { tokenData.tokenPrice = collectionPrice; } From a03eb19c0fd3e452b5a88c827aef002aa74aae3b Mon Sep 17 00:00:00 2001 From: GopherJ Date: Wed, 1 Feb 2023 09:10:22 +0800 Subject: [PATCH 03/19] fix: typo Signed-off-by: GopherJ --- contracts/protocol/libraries/logic/GenericLogic.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/protocol/libraries/logic/GenericLogic.sol b/contracts/protocol/libraries/logic/GenericLogic.sol index 0c0e7d666..7ea8950d1 100644 --- a/contracts/protocol/libraries/logic/GenericLogic.sol +++ b/contracts/protocol/libraries/logic/GenericLogic.sol @@ -383,7 +383,11 @@ library GenericLogic { (collateralizedBalance - atomicCollateralizedBalance) * assetPrice; - for (uint256 index = atomicBalance; index < balance; index++) { + for ( + uint256 index = balance - atomicBalance; + index < balance; + index++ + ) { uint256 tokenId = nToken.tokenOfOwnerByIndex(params.user, index); if ( ICollateralizableERC721(vars.xTokenAddress).isUsedAsCollateral( From 9abf26827d469caad6c302550ed4598855f4ec7f Mon Sep 17 00:00:00 2001 From: GopherJ Date: Wed, 1 Feb 2023 09:20:48 +0800 Subject: [PATCH 04/19] feat: seperate uni v3 calculation Signed-off-by: GopherJ --- .../protocol/libraries/logic/GenericLogic.sol | 73 +++++++++++-------- 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/contracts/protocol/libraries/logic/GenericLogic.sol b/contracts/protocol/libraries/logic/GenericLogic.sol index 7ea8950d1..65a76961b 100644 --- a/contracts/protocol/libraries/logic/GenericLogic.sol +++ b/contracts/protocol/libraries/logic/GenericLogic.sol @@ -365,47 +365,58 @@ library GenericLogic { CalculateUserAccountDataVars memory vars ) private view returns (uint256 totalValue) { INToken nToken = INToken(vars.xTokenAddress); - uint256 collateralizedBalance = ICollateralizableERC721( - vars.xTokenAddress - ).collateralizedBalanceOf(params.user); - uint256 atomicCollateralizedBalance = IAtomicCollateralizableERC721( - vars.xTokenAddress - ).atomicCollateralizedBalanceOf(params.user); uint256 balance = INToken(vars.xTokenAddress).balanceOf(params.user); - uint256 atomicBalance = IAtomicCollateralizableERC721( - vars.xTokenAddress - ).atomicBalanceOf(params.user); - uint256 assetPrice = _getAssetPrice( - params.oracle, - vars.currentReserveAddress - ); - totalValue = - (collateralizedBalance - atomicCollateralizedBalance) * - assetPrice; - for ( - uint256 index = balance - atomicBalance; - index < balance; - index++ + if ( + IXTokenType(vars.xTokenAddress).getXTokenType() == + XTokenType.NTokenUniswapV3 ) { - uint256 tokenId = nToken.tokenOfOwnerByIndex(params.user, index); - if ( - ICollateralizableERC721(vars.xTokenAddress).isUsedAsCollateral( - tokenId - ) - ) { + for (uint256 index = 0; index < balance; index++) { + uint256 tokenId = nToken.tokenOfOwnerByIndex( + params.user, + index + ); if ( - IXTokenType(vars.xTokenAddress).getXTokenType() == - XTokenType.NTokenUniswapV3 + ICollateralizableERC721(vars.xTokenAddress) + .isUsedAsCollateral(tokenId) ) { totalValue += _getTokenPrice( params.oracle, vars.currentReserveAddress, tokenId ); - } else if ( - IAtomicCollateralizableERC721(vars.xTokenAddress) - .isAtomicToken(tokenId) + } + } + } else { + uint256 collateralizedBalance = ICollateralizableERC721( + vars.xTokenAddress + ).collateralizedBalanceOf(params.user); + uint256 atomicCollateralizedBalance = IAtomicCollateralizableERC721( + vars.xTokenAddress + ).atomicCollateralizedBalanceOf(params.user); + uint256 atomicBalance = IAtomicCollateralizableERC721( + vars.xTokenAddress + ).atomicBalanceOf(params.user); + uint256 assetPrice = _getAssetPrice( + params.oracle, + vars.currentReserveAddress + ); + totalValue = + (collateralizedBalance - atomicCollateralizedBalance) * + assetPrice; + + for ( + uint256 index = balance - atomicBalance; + index < balance; + index++ + ) { + uint256 tokenId = nToken.tokenOfOwnerByIndex( + params.user, + index + ); + if ( + ICollateralizableERC721(vars.xTokenAddress) + .isUsedAsCollateral(tokenId) ) { uint256 multiplier = IAtomicCollateralizableERC721( vars.xTokenAddress From 17b9093ec110aafe60dfc2b3d4ada1194ea9e1b5 Mon Sep 17 00:00:00 2001 From: Cheng JIANG Date: Wed, 1 Feb 2023 09:40:11 +0800 Subject: [PATCH 05/19] Update contracts/interfaces/IAtomicCollateralizableERC721.sol Co-authored-by: Hong Liang --- contracts/interfaces/IAtomicCollateralizableERC721.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/interfaces/IAtomicCollateralizableERC721.sol b/contracts/interfaces/IAtomicCollateralizableERC721.sol index 5501900a3..a1161e1fe 100644 --- a/contracts/interfaces/IAtomicCollateralizableERC721.sol +++ b/contracts/interfaces/IAtomicCollateralizableERC721.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.10; /** * @title IAtomicCollateralizableERC721 * @author Parallel - * @notice Defines the basic interface for an CollateralizableERC721. + * @notice Defines the basic interface for an AtomicCollateralizableERC721. **/ interface IAtomicCollateralizableERC721 { /** From 85ec241718db1f4695e1e0699d72e7759683b8da Mon Sep 17 00:00:00 2001 From: GopherJ Date: Wed, 1 Feb 2023 10:05:13 +0800 Subject: [PATCH 06/19] fix: balance limit check Signed-off-by: GopherJ --- .../tokenization/libraries/MintableERC721Logic.sol | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol b/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol index bc60feb48..d218bce9e 100644 --- a/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol +++ b/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol @@ -145,11 +145,14 @@ library MintableERC721Logic { .atomicBalance; if (isAtomic) { uint64 newRecipientAtomicBalance = oldRecipientAtomicBalance + 1; - _checkAtomicBalanceLimit(erc721Data, newRecipientAtomicBalance); erc721Data.userState[to].atomicBalance = newRecipientAtomicBalance; } else { erc721Data.userState[to].balance = oldRecipientBalance + 1; } + _checkBalanceLimit( + erc721Data, + oldRecipientAtomicBalance + oldRecipientBalance + 1 + ); erc721Data.owners[tokenId] = to; if (from != to && erc721Data.auctions[tokenId].startTime > 0) { @@ -314,7 +317,7 @@ library MintableERC721Logic { .userState[to] .atomicCollateralizedBalance = newAtomicCollateralizedBalance; - _checkAtomicBalanceLimit(erc721Data, vars.oldAtomicBalance); + _checkBalanceLimit(erc721Data, vars.oldAtomicBalance + vars.oldBalance); erc721Data.userState[to].balance = vars.oldBalance; erc721Data.userState[to].atomicBalance = vars.oldAtomicBalance; @@ -567,13 +570,13 @@ library MintableERC721Logic { } } - function _checkAtomicBalanceLimit( + function _checkBalanceLimit( MintableERC721Data storage erc721Data, - uint64 atomicBalance + uint64 balance ) private view { uint64 balanceLimit = erc721Data.balanceLimit; require( - balanceLimit == 0 || atomicBalance <= balanceLimit, + balanceLimit == 0 || balance <= balanceLimit, Errors.NTOKEN_BALANCE_EXCEEDED ); } From 1f47cff4b72d69c3e004d3a8edba6d5207197ac5 Mon Sep 17 00:00:00 2001 From: GopherJ Date: Wed, 1 Feb 2023 13:10:12 +0800 Subject: [PATCH 07/19] fix: typo Signed-off-by: GopherJ --- .../protocol/tokenization/libraries/MintableERC721Logic.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol b/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol index d218bce9e..ebb950ffd 100644 --- a/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol +++ b/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol @@ -346,8 +346,8 @@ library MintableERC721Logic { ) external returns ( - uint64 oldCollateralizedBalance, - uint64 newCollateralizedBalance + uint64 oldTotalCollateralizedBalance, + uint64 newTotalCollateralizedBalance ) { LocalVars memory vars = _cache(erc721Data, user); @@ -422,7 +422,7 @@ library MintableERC721Logic { rewardControllerLocal.handleAction( user, oldTotalSupply, - vars.oldBalance + vars.oldAtomicBalance - tokenIds.length + vars.oldBalance + vars.oldAtomicBalance + tokenIds.length ); } From f64292ab6c51d76f1fb6d15518009b6cbfdfdeac Mon Sep 17 00:00:00 2001 From: GopherJ Date: Wed, 1 Feb 2023 13:19:26 +0800 Subject: [PATCH 08/19] chore: rename Signed-off-by: GopherJ --- .../libraries/MintableERC721Logic.sol | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol b/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol index ebb950ffd..162e59c5b 100644 --- a/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol +++ b/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol @@ -45,17 +45,17 @@ struct MintableERC721Data { uint64 balanceLimit; mapping(uint256 => bool) isUsedAsCollateral; mapping(uint256 => DataTypes.Auction) auctions; - // Mapping from owner to list of owned token IDs + // Mapping from owner to list of owned atomic token IDs mapping(address => mapping(uint256 => uint256)) ownedAtomicTokens; - // Mapping from token ID to index of the owner tokens list + // Mapping from token ID to index of the owned atomic tokens list mapping(uint256 => uint256) ownedAtomicTokensIndex; // All atomic tokens' traits multipliers mapping(uint256 => uint256) traitsMultipliers; } struct LocalVars { - uint64 oldBalance; - uint64 oldAtomicBalance; + uint64 balance; + uint64 atomicBalance; uint64 oldCollateralizedBalance; uint64 oldAtomicCollateralizedBalance; uint64 collateralizedTokens; @@ -280,13 +280,13 @@ library MintableERC721Logic { erc721Data, to, tokenId, - isAtomic ? vars.oldAtomicBalance : vars.oldBalance, + isAtomic ? vars.atomicBalance : vars.balance, isAtomic ); if (isAtomic) { - vars.oldAtomicBalance += 1; + vars.atomicBalance += 1; } else { - vars.oldBalance += 1; + vars.balance += 1; } erc721Data.owners[tokenId] = to; @@ -317,10 +317,10 @@ library MintableERC721Logic { .userState[to] .atomicCollateralizedBalance = newAtomicCollateralizedBalance; - _checkBalanceLimit(erc721Data, vars.oldAtomicBalance + vars.oldBalance); + _checkBalanceLimit(erc721Data, vars.atomicBalance + vars.balance); - erc721Data.userState[to].balance = vars.oldBalance; - erc721Data.userState[to].atomicBalance = vars.oldAtomicBalance; + erc721Data.userState[to].balance = vars.balance; + erc721Data.userState[to].atomicBalance = vars.atomicBalance; // calculate incentives IRewardController rewardControllerLocal = erc721Data.rewardController; @@ -328,7 +328,7 @@ library MintableERC721Logic { rewardControllerLocal.handleAction( to, oldTotalSupply, - vars.oldBalance + vars.oldAtomicBalance - tokenData.length + vars.balance + vars.atomicBalance - tokenData.length ); } @@ -372,13 +372,13 @@ library MintableERC721Logic { erc721Data, user, tokenId, - isAtomic ? vars.oldAtomicBalance : vars.oldBalance, + isAtomic ? vars.atomicBalance : vars.balance, isAtomic ); if (isAtomic) { - vars.oldAtomicBalance -= 1; + vars.atomicBalance -= 1; } else { - vars.oldBalance -= 1; + vars.balance -= 1; } // Clear approvals @@ -401,8 +401,8 @@ library MintableERC721Logic { emit Transfer(owner, address(0), tokenId); } - erc721Data.userState[user].balance = vars.oldBalance; - erc721Data.userState[user].atomicBalance = vars.oldAtomicBalance; + erc721Data.userState[user].balance = vars.balance; + erc721Data.userState[user].atomicBalance = vars.atomicBalance; uint64 newCollateralizedBalance = vars.oldCollateralizedBalance - vars.collateralizedTokens; @@ -422,7 +422,7 @@ library MintableERC721Logic { rewardControllerLocal.handleAction( user, oldTotalSupply, - vars.oldBalance + vars.oldAtomicBalance + tokenIds.length + vars.balance + vars.atomicBalance + tokenIds.length ); } @@ -602,8 +602,8 @@ library MintableERC721Logic { view returns (LocalVars memory vars) { - vars.oldBalance = erc721Data.userState[user].balance; - vars.oldAtomicBalance = erc721Data.userState[user].atomicBalance; + vars.balance = erc721Data.userState[user].balance; + vars.atomicBalance = erc721Data.userState[user].atomicBalance; vars.oldCollateralizedBalance = erc721Data .userState[user] .collateralizedBalance; From d908928151ee2c83e0980f858263b05f69fab538 Mon Sep 17 00:00:00 2001 From: GopherJ Date: Wed, 1 Feb 2023 15:15:03 +0800 Subject: [PATCH 09/19] feat: better atomic limit & api Signed-off-by: GopherJ --- .../IAtomicCollateralizableERC721.sol | 26 ++++++ contracts/mocks/upgradeability/MockNToken.sol | 2 +- .../protocol/libraries/helpers/Helpers.sol | 26 ++++++ .../protocol/libraries/logic/GenericLogic.sol | 84 +++++++------------ .../libraries/logic/LiquidationLogic.sol | 18 ++-- contracts/protocol/tokenization/NToken.sol | 9 +- .../tokenization/NTokenApeStaking.sol | 2 +- .../protocol/tokenization/NTokenBAKC.sol | 2 +- .../protocol/tokenization/NTokenMoonBirds.sol | 2 +- .../protocol/tokenization/NTokenUniswapV3.sol | 2 +- .../base/MintableIncentivizedERC721.sol | 64 +++++++++++++- .../libraries/MintableERC721Logic.sol | 47 ++++++----- contracts/ui/UiPoolDataProvider.sol | 22 ++--- .../ui/interfaces/IUiPoolDataProvider.sol | 1 + 14 files changed, 199 insertions(+), 108 deletions(-) diff --git a/contracts/interfaces/IAtomicCollateralizableERC721.sol b/contracts/interfaces/IAtomicCollateralizableERC721.sol index a1161e1fe..d3dfb5165 100644 --- a/contracts/interfaces/IAtomicCollateralizableERC721.sol +++ b/contracts/interfaces/IAtomicCollateralizableERC721.sol @@ -20,11 +20,37 @@ interface IAtomicCollateralizableERC721 { */ function atomicBalanceOf(address user) external view returns (uint256); + /** + * @dev get the token balance of a specific user + */ + function balancesOf(address user) + external + view + returns ( + uint256, + uint256, + uint256, + uint256 + ); + + /** + * @dev get the atomic token id of a specific user of specific index + */ + function atomicTokenOfOwnerByIndex(address user, uint256 index) + external + view + returns (uint256); + /** * @dev check if specific token is atomic (has multiplier) */ function isAtomicToken(uint256 tokenId) external view returns (bool); + /** + * @dev check if specific token has atomic pricing (has atomic oracle wrapper) + */ + function isAtomicPricing() external view returns (bool); + /** * @dev get the trait multiplier of specific token */ diff --git a/contracts/mocks/upgradeability/MockNToken.sol b/contracts/mocks/upgradeability/MockNToken.sol index 2aa8b5923..66e23e9d7 100644 --- a/contracts/mocks/upgradeability/MockNToken.sol +++ b/contracts/mocks/upgradeability/MockNToken.sol @@ -6,7 +6,7 @@ import {IPool} from "../../interfaces/IPool.sol"; import {IRewardController} from "../../interfaces/IRewardController.sol"; contract MockNToken is NToken { - constructor(IPool pool) NToken(pool) {} + constructor(IPool pool) NToken(pool, false) {} function getRevision() internal pure override returns (uint256) { return 999; diff --git a/contracts/protocol/libraries/helpers/Helpers.sol b/contracts/protocol/libraries/helpers/Helpers.sol index 964cdb16b..feef75fee 100644 --- a/contracts/protocol/libraries/helpers/Helpers.sol +++ b/contracts/protocol/libraries/helpers/Helpers.sol @@ -3,12 +3,16 @@ pragma solidity 0.8.10; 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"; /** * @title Helpers library * */ library Helpers { + using WadRayMath for uint256; + /** * @notice Fetches the user current stable and variable debt balances * @param user The user address @@ -22,4 +26,26 @@ library Helpers { { return (IERC20(debtTokenAddress).balanceOf(user)); } + + function isTraitMultiplierEffective(uint256 multiplier) + internal + pure + returns (bool) + { + return multiplier != 0 && multiplier != WadRayMath.WAD; + } + + function getTraitBoostedTokenPrice( + address xTokenAddress, + uint256 assetPrice, + uint256 tokenId + ) internal view returns (uint256) { + uint256 multiplier = IAtomicCollateralizableERC721(xTokenAddress) + .getTraitMultiplier(tokenId); + if (isTraitMultiplierEffective(multiplier)) { + return assetPrice.wadMul(multiplier); + } else { + return assetPrice; + } + } } diff --git a/contracts/protocol/libraries/logic/GenericLogic.sol b/contracts/protocol/libraries/logic/GenericLogic.sol index 65a76961b..7859fa3bc 100644 --- a/contracts/protocol/libraries/logic/GenericLogic.sol +++ b/contracts/protocol/libraries/logic/GenericLogic.sol @@ -17,6 +17,7 @@ import {DataTypes} from "../types/DataTypes.sol"; import {ReserveLogic} from "./ReserveLogic.sol"; import {INonfungiblePositionManager} from "../../../dependencies/uniswap/INonfungiblePositionManager.sol"; import {XTokenType, IXTokenType} from "../../../interfaces/IXTokenType.sol"; +import {Helpers} from "../../libraries/helpers/Helpers.sol"; /** * @title GenericLogic library @@ -364,65 +365,36 @@ library GenericLogic { DataTypes.CalculateUserAccountDataParams memory params, CalculateUserAccountDataVars memory vars ) private view returns (uint256 totalValue) { - INToken nToken = INToken(vars.xTokenAddress); - uint256 balance = INToken(vars.xTokenAddress).balanceOf(params.user); - - if ( - IXTokenType(vars.xTokenAddress).getXTokenType() == - XTokenType.NTokenUniswapV3 - ) { - for (uint256 index = 0; index < balance; index++) { - uint256 tokenId = nToken.tokenOfOwnerByIndex( - params.user, - index - ); - if ( - ICollateralizableERC721(vars.xTokenAddress) - .isUsedAsCollateral(tokenId) - ) { - totalValue += _getTokenPrice( - params.oracle, - vars.currentReserveAddress, - tokenId - ); - } - } - } else { - uint256 collateralizedBalance = ICollateralizableERC721( - vars.xTokenAddress - ).collateralizedBalanceOf(params.user); - uint256 atomicCollateralizedBalance = IAtomicCollateralizableERC721( - vars.xTokenAddress - ).atomicCollateralizedBalanceOf(params.user); - uint256 atomicBalance = IAtomicCollateralizableERC721( - vars.xTokenAddress - ).atomicBalanceOf(params.user); - uint256 assetPrice = _getAssetPrice( - params.oracle, - vars.currentReserveAddress + uint256 assetPrice = _getAssetPrice( + params.oracle, + vars.currentReserveAddress + ); + + ( + , + uint256 atomicBalance, + uint256 collateralizedBalance, + uint256 atomicCollateralizedBalance + ) = IAtomicCollateralizableERC721(vars.xTokenAddress).balancesOf( + params.user ); - totalValue = - (collateralizedBalance - atomicCollateralizedBalance) * - assetPrice; - - for ( - uint256 index = balance - atomicBalance; - index < balance; - index++ + totalValue = + (collateralizedBalance - atomicCollateralizedBalance) * + assetPrice; + + for (uint256 index = 0; index < atomicBalance; index++) { + uint256 tokenId = IAtomicCollateralizableERC721(vars.xTokenAddress) + .atomicTokenOfOwnerByIndex(params.user, index); + if ( + ICollateralizableERC721(vars.xTokenAddress).isUsedAsCollateral( + tokenId + ) ) { - uint256 tokenId = nToken.tokenOfOwnerByIndex( - params.user, - index + totalValue += Helpers.getTraitBoostedTokenPrice( + vars.xTokenAddress, + assetPrice, + tokenId ); - if ( - ICollateralizableERC721(vars.xTokenAddress) - .isUsedAsCollateral(tokenId) - ) { - uint256 multiplier = IAtomicCollateralizableERC721( - vars.xTokenAddress - ).getTraitMultiplier(tokenId); - totalValue += assetPrice.wadMul(multiplier); - } } } } diff --git a/contracts/protocol/libraries/logic/LiquidationLogic.sol b/contracts/protocol/libraries/logic/LiquidationLogic.sol index bc344feef..1b7dbbc68 100644 --- a/contracts/protocol/libraries/logic/LiquidationLogic.sol +++ b/contracts/protocol/libraries/logic/LiquidationLogic.sol @@ -788,19 +788,15 @@ library LiquidationLogic { params.collateralAsset, params.collateralTokenId ); - } else if ( - IAtomicCollateralizableERC721(superVars.collateralXToken) - .isAtomicToken(params.collateralTokenId) - ) { - uint256 multiplier = IAtomicCollateralizableERC721( - superVars.collateralXToken - ).getTraitMultiplier(params.collateralTokenId); - vars.collateralPrice = IPriceOracleGetter(params.priceOracle) - .getAssetPrice(params.collateralAsset) - .wadMul(multiplier); } else { - vars.collateralPrice = IPriceOracleGetter(params.priceOracle) + uint256 assetPrice = IPriceOracleGetter(params.priceOracle) .getAssetPrice(params.collateralAsset); + + vars.collateralPrice = Helpers.getTraitBoostedTokenPrice( + superVars.collateralXToken, + assetPrice, + params.collateralTokenId + ); } if ( diff --git a/contracts/protocol/tokenization/NToken.sol b/contracts/protocol/tokenization/NToken.sol index 83f6274ec..0ff3f812e 100644 --- a/contracts/protocol/tokenization/NToken.sol +++ b/contracts/protocol/tokenization/NToken.sol @@ -40,8 +40,13 @@ contract NToken is VersionedInitializable, MintableIncentivizedERC721, INToken { * @dev Constructor. * @param pool The address of the Pool contract */ - constructor(IPool pool) - MintableIncentivizedERC721(pool, "NTOKEN_IMPL", "NTOKEN_IMPL") + constructor(IPool pool, bool atomic_pricing) + MintableIncentivizedERC721( + pool, + "NTOKEN_IMPL", + "NTOKEN_IMPL", + atomic_pricing + ) {} function initialize( diff --git a/contracts/protocol/tokenization/NTokenApeStaking.sol b/contracts/protocol/tokenization/NTokenApeStaking.sol index db0d492b1..37ed58c8c 100644 --- a/contracts/protocol/tokenization/NTokenApeStaking.sol +++ b/contracts/protocol/tokenization/NTokenApeStaking.sol @@ -36,7 +36,7 @@ abstract contract NTokenApeStaking is NToken, INTokenApeStaking { * @dev Constructor. * @param pool The address of the Pool contract */ - constructor(IPool pool, address apeCoinStaking) NToken(pool) { + constructor(IPool pool, address apeCoinStaking) NToken(pool, false) { _apeCoinStaking = ApeCoinStaking(apeCoinStaking); } diff --git a/contracts/protocol/tokenization/NTokenBAKC.sol b/contracts/protocol/tokenization/NTokenBAKC.sol index 89e5ee162..d83c191ed 100644 --- a/contracts/protocol/tokenization/NTokenBAKC.sol +++ b/contracts/protocol/tokenization/NTokenBAKC.sol @@ -35,7 +35,7 @@ contract NTokenBAKC is NToken { address apeCoinStaking, address _nBAYC, address _nMAYC - ) NToken(pool) { + ) NToken(pool, false) { _apeCoinStaking = ApeCoinStaking(apeCoinStaking); nBAYC = _nBAYC; nMAYC = _nMAYC; diff --git a/contracts/protocol/tokenization/NTokenMoonBirds.sol b/contracts/protocol/tokenization/NTokenMoonBirds.sol index 66439426f..52ca79710 100644 --- a/contracts/protocol/tokenization/NTokenMoonBirds.sol +++ b/contracts/protocol/tokenization/NTokenMoonBirds.sol @@ -29,7 +29,7 @@ contract NTokenMoonBirds is NToken, IMoonBirdBase { * @dev Constructor. * @param pool The address of the Pool contract */ - constructor(IPool pool) NToken(pool) { + constructor(IPool pool) NToken(pool, false) { // Intentionally left blank } diff --git a/contracts/protocol/tokenization/NTokenUniswapV3.sol b/contracts/protocol/tokenization/NTokenUniswapV3.sol index 6b3451f50..3a3aa3187 100644 --- a/contracts/protocol/tokenization/NTokenUniswapV3.sol +++ b/contracts/protocol/tokenization/NTokenUniswapV3.sol @@ -30,7 +30,7 @@ contract NTokenUniswapV3 is NToken, INTokenUniswapV3 { * @dev Constructor. * @param pool The address of the Pool contract */ - constructor(IPool pool) NToken(pool) { + constructor(IPool pool) NToken(pool, true) { _ERC721Data.balanceLimit = 30; } diff --git a/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol b/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol index 2f28fc07a..f4d07cf75 100644 --- a/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol +++ b/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol @@ -73,6 +73,7 @@ abstract contract MintableIncentivizedERC721 is IPoolAddressesProvider internal immutable _addressesProvider; IPool public immutable POOL; + bool public immutable ATOMIC_PRICING; address internal _underlyingAsset; @@ -85,12 +86,14 @@ abstract contract MintableIncentivizedERC721 is constructor( IPool pool, string memory name_, - string memory symbol_ + string memory symbol_, + bool atomic_pricing ) { _addressesProvider = pool.ADDRESSES_PROVIDER(); _ERC721Data.name = name_; _ERC721Data.symbol = symbol_; POOL = pool; + ATOMIC_PRICING = atomic_pricing; } function name() public view override returns (string memory) { @@ -113,6 +116,26 @@ abstract contract MintableIncentivizedERC721 is _ERC721Data.userState[account].atomicBalance; } + function balancesOf(address account) + public + view + virtual + override + returns ( + uint256, + uint256, + uint256, + uint256 + ) + { + return ( + _ERC721Data.userState[account].balance, + _ERC721Data.userState[account].atomicBalance, + _ERC721Data.userState[account].collateralizedBalance, + _ERC721Data.userState[account].atomicCollateralizedBalance + ); + } + /** * @notice Returns the address of the Incentives Controller contract * @return The address of the Incentives Controller @@ -375,7 +398,12 @@ abstract contract MintableIncentivizedERC721 is ) { return - MintableERC721Logic.executeMintMultiple(_ERC721Data, to, tokenData); + MintableERC721Logic.executeMintMultiple( + _ERC721Data, + ATOMIC_PRICING, + to, + tokenData + ); } function _burnMultiple(address user, uint256[] calldata tokenIds) @@ -390,6 +418,7 @@ abstract contract MintableIncentivizedERC721 is MintableERC721Logic.executeBurnMultiple( _ERC721Data, POOL, + ATOMIC_PRICING, user, tokenIds ); @@ -414,6 +443,7 @@ abstract contract MintableIncentivizedERC721 is MintableERC721Logic.executeTransfer( _ERC721Data, POOL, + ATOMIC_PRICING, from, to, tokenId @@ -432,6 +462,7 @@ abstract contract MintableIncentivizedERC721 is .executeTransferCollateralizable( _ERC721Data, POOL, + ATOMIC_PRICING, from, to, tokenId @@ -483,6 +514,7 @@ abstract contract MintableIncentivizedERC721 is MintableERC721Logic.executeSetIsUsedAsCollateral( _ERC721Data, POOL, + ATOMIC_PRICING, tokenId, useAsCollateral, sender @@ -513,6 +545,7 @@ abstract contract MintableIncentivizedERC721 is MintableERC721Logic.executeSetIsUsedAsCollateral( _ERC721Data, POOL, + ATOMIC_PRICING, tokenIds[index], useAsCollateral, sender @@ -551,7 +584,17 @@ abstract contract MintableIncentivizedERC721 is virtual returns (bool) { - return MintableERC721Logic.isAtomicToken(_ERC721Data, tokenId); + return + MintableERC721Logic.isAtomicToken( + _ERC721Data, + ATOMIC_PRICING, + tokenId + ); + } + + /// @inheritdoc IAtomicCollateralizableERC721 + function isAtomicPricing() external view virtual returns (bool) { + return ATOMIC_PRICING; } /// @inheritdoc IAtomicCollateralizableERC721 @@ -655,6 +698,21 @@ abstract contract MintableIncentivizedERC721 is } } + function atomicTokenOfOwnerByIndex(address owner, uint256 index) + external + view + virtual + override + returns (uint256) + { + uint256 atomicBalance = _ERC721Data.userState[owner].atomicBalance; + require( + index < atomicBalance, + "ERC721Enumerable: owner index out of bounds" + ); + return _ERC721Data.ownedAtomicTokens[owner][index]; + } + /** * @dev See {IERC721Enumerable-totalSupply}. */ diff --git a/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol b/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol index 162e59c5b..8c8c88c48 100644 --- a/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol +++ b/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol @@ -4,6 +4,7 @@ import {ApeCoinStaking} from "../../../dependencies/yoga-labs/ApeCoinStaking.sol import {IERC721} from "../../../dependencies/openzeppelin/contracts/IERC721.sol"; import {SafeERC20} from "../../../dependencies/openzeppelin/contracts/SafeERC20.sol"; import {WadRayMath} from "../../libraries/math/WadRayMath.sol"; +import {Helpers} from "../../libraries/helpers/Helpers.sol"; import {IERC20} from "../../../dependencies/openzeppelin/contracts/IERC20.sol"; import "../../../interfaces/IRewardController.sol"; import "../../libraries/types/DataTypes.sol"; @@ -77,7 +78,7 @@ library MintableERC721Logic { * @dev This constant represents the minimum trait multiplier that a single tokenId can have * A value of 1e18 results in no price multiplier */ - uint256 internal constant MIN_TRAIT_MULTIPLIER = 0.5e18; + uint256 internal constant MIN_TRAIT_MULTIPLIER = 0e18; /** * @dev Emitted when `tokenId` token is transferred from `from` to `to`. @@ -109,6 +110,7 @@ library MintableERC721Logic { function executeTransfer( MintableERC721Data storage erc721Data, IPool POOL, + bool atomic_pricing, address from, address to, uint256 tokenId @@ -122,7 +124,7 @@ library MintableERC721Logic { !isAuctioned(erc721Data, POOL, tokenId), Errors.TOKEN_IN_AUCTION ); - bool isAtomic = isAtomicToken(erc721Data, tokenId); + bool isAtomic = isAtomicToken(erc721Data, atomic_pricing, tokenId); _beforeTokenTransfer(erc721Data, from, to, tokenId, isAtomic); // Clear approvals from the previous owner @@ -145,14 +147,11 @@ library MintableERC721Logic { .atomicBalance; if (isAtomic) { uint64 newRecipientAtomicBalance = oldRecipientAtomicBalance + 1; + _checkAtomicBalanceLimit(erc721Data, newRecipientAtomicBalance); erc721Data.userState[to].atomicBalance = newRecipientAtomicBalance; } else { erc721Data.userState[to].balance = oldRecipientBalance + 1; } - _checkBalanceLimit( - erc721Data, - oldRecipientAtomicBalance + oldRecipientBalance + 1 - ); erc721Data.owners[tokenId] = to; if (from != to && erc721Data.auctions[tokenId].startTime > 0) { @@ -182,6 +181,7 @@ library MintableERC721Logic { function executeTransferCollateralizable( MintableERC721Data storage erc721Data, IPool POOL, + bool atomic_pricing, address from, address to, uint256 tokenId @@ -189,7 +189,7 @@ library MintableERC721Logic { isUsedAsCollateral_ = erc721Data.isUsedAsCollateral[tokenId]; if (from != to && isUsedAsCollateral_) { - if (isAtomicToken(erc721Data, tokenId)) { + if (isAtomicToken(erc721Data, atomic_pricing, tokenId)) { erc721Data.userState[from].atomicCollateralizedBalance -= 1; } else { erc721Data.userState[from].collateralizedBalance -= 1; @@ -197,12 +197,13 @@ library MintableERC721Logic { delete erc721Data.isUsedAsCollateral[tokenId]; } - executeTransfer(erc721Data, POOL, from, to, tokenId); + executeTransfer(erc721Data, POOL, atomic_pricing, from, to, tokenId); } function executeSetIsUsedAsCollateral( MintableERC721Data storage erc721Data, IPool POOL, + bool atomic_pricing, uint256 tokenId, bool useAsCollateral, address sender @@ -220,7 +221,7 @@ library MintableERC721Logic { ); } - if (isAtomicToken(erc721Data, tokenId)) { + if (isAtomicToken(erc721Data, atomic_pricing, tokenId)) { uint64 collateralizedBalance = erc721Data .userState[owner] .atomicCollateralizedBalance; @@ -249,6 +250,7 @@ library MintableERC721Logic { function executeMintMultiple( MintableERC721Data storage erc721Data, + bool atomic_pricing, address to, DataTypes.ERC721SupplyParams[] calldata tokenData ) @@ -275,7 +277,7 @@ library MintableERC721Logic { tokenId, oldTotalSupply + index ); - bool isAtomic = isAtomicToken(erc721Data, tokenId); + bool isAtomic = isAtomicToken(erc721Data, atomic_pricing, tokenId); _addTokenToOwnerEnumeration( erc721Data, to, @@ -317,7 +319,7 @@ library MintableERC721Logic { .userState[to] .atomicCollateralizedBalance = newAtomicCollateralizedBalance; - _checkBalanceLimit(erc721Data, vars.atomicBalance + vars.balance); + _checkAtomicBalanceLimit(erc721Data, vars.atomicBalance); erc721Data.userState[to].balance = vars.balance; erc721Data.userState[to].atomicBalance = vars.atomicBalance; @@ -341,6 +343,7 @@ library MintableERC721Logic { function executeBurnMultiple( MintableERC721Data storage erc721Data, IPool POOL, + bool atomic_pricing, address user, uint256[] calldata tokenIds ) @@ -367,7 +370,7 @@ library MintableERC721Logic { tokenId, oldTotalSupply - index ); - bool isAtomic = isAtomicToken(erc721Data, tokenId); + bool isAtomic = isAtomicToken(erc721Data, atomic_pricing, tokenId); _removeTokenFromOwnerEnumeration( erc721Data, user, @@ -512,9 +515,12 @@ library MintableERC721Logic { continue; } - bool isAtomicPrev = oldMultiplier != 0 && - oldMultiplier != WadRayMath.WAD; - bool isAtomicNext = multipliers[i] != WadRayMath.WAD; + bool isAtomicPrev = Helpers.isTraitMultiplierEffective( + oldMultiplier + ); + bool isAtomicNext = Helpers.isTraitMultiplierEffective( + multipliers[i] + ); if (isAtomicPrev && !isAtomicNext) { _removeTokenFromOwnerEnumeration( @@ -570,20 +576,20 @@ library MintableERC721Logic { } } - function _checkBalanceLimit( + function _checkAtomicBalanceLimit( MintableERC721Data storage erc721Data, - uint64 balance + uint64 atomicBalance ) private view { uint64 balanceLimit = erc721Data.balanceLimit; require( - balanceLimit == 0 || balance <= balanceLimit, + balanceLimit == 0 || atomicBalance <= balanceLimit, Errors.NTOKEN_BALANCE_EXCEEDED ); } function _checkTraitMultiplier(uint256 multiplier) private pure { require( - multiplier > MIN_TRAIT_MULTIPLIER && + multiplier >= MIN_TRAIT_MULTIPLIER && multiplier < MAX_TRAIT_MULTIPLIER, Errors.INVALID_AMOUNT ); @@ -614,10 +620,11 @@ library MintableERC721Logic { function isAtomicToken( MintableERC721Data storage erc721Data, + bool atomic_pricing, uint256 tokenId ) public view returns (bool) { uint256 multiplier = erc721Data.traitsMultipliers[tokenId]; - return multiplier != 0 && multiplier != WadRayMath.WAD; + return atomic_pricing || Helpers.isTraitMultiplierEffective(multiplier); } function isAuctioned( diff --git a/contracts/ui/UiPoolDataProvider.sol b/contracts/ui/UiPoolDataProvider.sol index 23139c304..9b089012a 100644 --- a/contracts/ui/UiPoolDataProvider.sol +++ b/contracts/ui/UiPoolDataProvider.sol @@ -26,6 +26,7 @@ import {ProtocolDataProvider} from "../misc/ProtocolDataProvider.sol"; import {DataTypes} from "../protocol/libraries/types/DataTypes.sol"; import {IUniswapV3OracleWrapper} from "../interfaces/IUniswapV3OracleWrapper.sol"; import {UinswapV3PositionData} from "../interfaces/IUniswapV3PositionInfoProvider.sol"; +import {Helpers} from "../protocol/libraries/helpers/Helpers.sol"; contract UiPoolDataProvider is IUiPoolDataProvider { using WadRayMath for uint256; @@ -180,6 +181,9 @@ contract UiPoolDataProvider is IUiPoolDataProvider { reserveData.availableLiquidity = IERC721( reserveData.underlyingAsset ).balanceOf(reserveData.xTokenAddress); + reserveData.isAtomicPricing = IAtomicCollateralizableERC721( + reserveData.xTokenAddress + ).isAtomicPricing(); } ( @@ -494,24 +498,20 @@ contract UiPoolDataProvider is IUiPoolDataProvider { ); // token price if ( - IXTokenType(baseData.xTokenAddress).getXTokenType() == - XTokenType.NTokenUniswapV3 + IAtomicCollateralizableERC721(baseData.xTokenAddress) + .isAtomicPricing() ) { try oracle.getTokenPrice(tokenData.asset, tokenData.tokenId) returns (uint256 price) { tokenData.tokenPrice = price; } catch {} - } else if ( - IAtomicCollateralizableERC721(baseData.xTokenAddress) - .isAtomicToken(tokenData.tokenId) - ) { - uint256 multiplier = IAtomicCollateralizableERC721( - baseData.xTokenAddress - ).getTraitMultiplier(tokenData.tokenId); - tokenData.tokenPrice = collectionPrice.wadMul(multiplier); } else { - tokenData.tokenPrice = collectionPrice; + tokenData.tokenPrice = Helpers.getTraitBoostedTokenPrice( + baseData.xTokenAddress, + collectionPrice, + tokenData.tokenId + ); } // token auction data tokenData.auctionData = pool.getAuctionData( diff --git a/contracts/ui/interfaces/IUiPoolDataProvider.sol b/contracts/ui/interfaces/IUiPoolDataProvider.sol index 54cfa103a..9947e260e 100644 --- a/contracts/ui/interfaces/IUiPoolDataProvider.sol +++ b/contracts/ui/interfaces/IUiPoolDataProvider.sol @@ -27,6 +27,7 @@ interface IUiPoolDataProvider { bool isActive; bool isFrozen; bool isPaused; + bool isAtomicPricing; // base data uint128 liquidityIndex; uint128 variableBorrowIndex; From 14284dcae5034258a2e22d2c5253e3b7c592b18e Mon Sep 17 00:00:00 2001 From: GopherJ Date: Wed, 1 Feb 2023 16:44:57 +0800 Subject: [PATCH 10/19] fix: typo & add basic tests Signed-off-by: GopherJ --- .../protocol/libraries/logic/GenericLogic.sol | 6 +- test/_xtoken_ntoken.spec.ts | 112 ++++++++++++++++++ test/helpers/validated-steps.ts | 31 +++-- 3 files changed, 135 insertions(+), 14 deletions(-) diff --git a/contracts/protocol/libraries/logic/GenericLogic.sol b/contracts/protocol/libraries/logic/GenericLogic.sol index 7859fa3bc..18b826f5c 100644 --- a/contracts/protocol/libraries/logic/GenericLogic.sol +++ b/contracts/protocol/libraries/logic/GenericLogic.sol @@ -374,13 +374,11 @@ library GenericLogic { , uint256 atomicBalance, uint256 collateralizedBalance, - uint256 atomicCollateralizedBalance + ) = IAtomicCollateralizableERC721(vars.xTokenAddress).balancesOf( params.user ); - totalValue = - (collateralizedBalance - atomicCollateralizedBalance) * - assetPrice; + totalValue = collateralizedBalance * assetPrice; for (uint256 index = 0; index < atomicBalance; index++) { uint256 tokenId = IAtomicCollateralizableERC721(vars.xTokenAddress) diff --git a/test/_xtoken_ntoken.spec.ts b/test/_xtoken_ntoken.spec.ts index d8261070f..a134fee28 100644 --- a/test/_xtoken_ntoken.spec.ts +++ b/test/_xtoken_ntoken.spec.ts @@ -1,6 +1,9 @@ import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; import {expect} from "chai"; +import {HALF_WAD} from "../helpers/constants"; +import {waitForTx} from "../helpers/misc-utils"; import {testEnvFixture} from "./helpers/setup-env"; +import {supplyAndValidate} from "./helpers/validated-steps"; describe("NToken general", async () => { it("TC-ntoken-01: NToken is ERC721 compatible", async () => { @@ -13,4 +16,113 @@ describe("NToken general", async () => { expect(await nUniswapV3.supportsInterface("0x80ac58cd")).to.be.true; expect(await nMOONBIRD.supportsInterface("0x80ac58cd")).to.be.true; }); + + it("TC-ntoken-02: NToken atomic balance is correct when mint", async () => { + const { + nBAYC, + bayc, + users: [user1], + poolAdmin, + } = await loadFixture(testEnvFixture); + await waitForTx( + await nBAYC + .connect(poolAdmin.signer) + .setTraitsMultipliers(["0"], [HALF_WAD]) + ); + expect(await nBAYC.getTraitMultiplier("0")).eq(HALF_WAD); + expect(await nBAYC.isAtomicToken("0")).to.true; + + await supplyAndValidate(bayc, "1", user1, true); + + expect(await nBAYC.atomicBalanceOf(user1.address)).to.be.eq(1); + expect(await nBAYC.atomicCollateralizedBalanceOf(user1.address)).to.be.eq( + 1 + ); + }); + + it("TC-ntoken-03: NToken atomic balance is correct when setIsUsedAsCollateral", async () => { + const { + nBAYC, + bayc, + pool, + users: [user1], + poolAdmin, + } = await loadFixture(testEnvFixture); + await waitForTx( + await nBAYC + .connect(poolAdmin.signer) + .setTraitsMultipliers(["0"], [HALF_WAD]) + ); + expect(await nBAYC.getTraitMultiplier("0")).eq(HALF_WAD); + expect(await nBAYC.isAtomicToken("0")).to.true; + + await supplyAndValidate(bayc, "1", user1, true); + + await waitForTx( + await pool + .connect(user1.signer) + .setUserUseERC721AsCollateral(bayc.address, ["0"], false) + ); + + expect(await nBAYC.atomicBalanceOf(user1.address)).to.be.eq(1); + expect(await nBAYC.atomicCollateralizedBalanceOf(user1.address)).to.be.eq( + 0 + ); + }); + + it("TC-ntoken-04: NToken atomic balance is correct when trait multiplier got removed", async () => { + const { + nBAYC, + bayc, + users: [user1], + poolAdmin, + } = await loadFixture(testEnvFixture); + await waitForTx( + await nBAYC + .connect(poolAdmin.signer) + .setTraitsMultipliers(["0"], [HALF_WAD]) + ); + expect(await nBAYC.getTraitMultiplier("0")).eq(HALF_WAD); + expect(await nBAYC.isAtomicToken("0")).to.true; + + await supplyAndValidate(bayc, "1", user1, true); + + await waitForTx( + await nBAYC.connect(poolAdmin.signer).setTraitsMultipliers(["0"], ["0"]) + ); + + expect(await nBAYC.atomicBalanceOf(user1.address)).to.be.eq(0); + expect(await nBAYC.atomicCollateralizedBalanceOf(user1.address)).to.be.eq( + 0 + ); + }); + + it("TC-ntoken-05: NToken atomic balance is correct when transfer", async () => { + const { + nBAYC, + bayc, + users: [user1, user2], + poolAdmin, + } = await loadFixture(testEnvFixture); + await waitForTx( + await nBAYC + .connect(poolAdmin.signer) + .setTraitsMultipliers(["0"], [HALF_WAD]) + ); + expect(await nBAYC.getTraitMultiplier("0")).eq(HALF_WAD); + expect(await nBAYC.isAtomicToken("0")).to.true; + + await supplyAndValidate(bayc, "1", user1, true); + + await waitForTx( + await nBAYC + .connect(user1.signer) + .transferFrom(user1.address, user2.address, "0") + ); + + expect(await nBAYC.atomicBalanceOf(user2.address)).to.be.eq(1); + expect(await nBAYC.atomicCollateralizedBalanceOf(user2.address)).to.be.eq( + 0 + ); + }); }); diff --git a/test/helpers/validated-steps.ts b/test/helpers/validated-steps.ts index 05240c44d..efacf9de4 100644 --- a/test/helpers/validated-steps.ts +++ b/test/helpers/validated-steps.ts @@ -97,7 +97,6 @@ export const supplyAndValidate = async ( const pool = await getPoolProxy(); const protocolDataProvider = await getProtocolDataProvider(); const paraSpaceOracle = await getParaSpaceOracle(); - const deployer = await getDeployer(); const nftIdsToUse = isNFT ? [...Array(+amount).keys()] : null; if (mintTokens) { @@ -108,16 +107,14 @@ export const supplyAndValidate = async ( // approve protocol to access user wallet await approveTnx(token, isNFT, pool, user); - const pTokenAddress = ( + const xTokenAddress = ( await protocolDataProvider.getReserveTokensAddresses(token.address) ).xTokenAddress; - const pToken = await getPToken(pTokenAddress); + const xToken = await getPToken(xTokenAddress); - const assetPrice = await paraSpaceOracle - .connect(deployer.signer) - .getAssetPrice(token.address); + const assetPrice = await paraSpaceOracle.getAssetPrice(token.address); const tokenBalanceBefore = await token.balanceOf(user.address); - const pTokenBalanceBefore = await pToken.balanceOf(user.address); + const xTokenBalanceBefore = await xToken.balanceOf(user.address); const totalCollateralBefore = (await pool.getUserAccountData(user.address)) .totalCollateralBase; const availableToBorrowBefore = (await pool.getUserAccountData(user.address)) @@ -155,13 +152,27 @@ export const supplyAndValidate = async ( expect(tokenBalance).to.be.equal(tokenBalanceBefore.sub(amountInBaseUnits)); // check pToken balance increased in the deposited amount - const pTokenBalance = await pToken.balanceOf(user.address); - expect(pTokenBalance).to.be.equal(pTokenBalanceBefore.add(amountInBaseUnits)); + const xTokenBalance = await xToken.balanceOf(user.address); + expect(xTokenBalance).to.be.equal(xTokenBalanceBefore.add(amountInBaseUnits)); // asset is used as collateral, so total collateral increases in supplied amount const totalCollateral = (await pool.getUserAccountData(user.address)) .totalCollateralBase; - const depositedAmountInBaseUnits = BigNumber.from(amount).mul(assetPrice); + let depositedAmountInBaseUnits; + if (isNFT && nftIdsToUse) { + const multiplier = await ( + await getNToken(xTokenAddress) + ).getTraitMultiplier(nftIdsToUse[0]); + if (!multiplier.eq(0) && !multiplier.eq(WAD)) { + depositedAmountInBaseUnits = BigNumber.from(amount) + .mul(assetPrice) + .wadMul(multiplier); + } else { + depositedAmountInBaseUnits = BigNumber.from(amount).mul(assetPrice); + } + } else { + depositedAmountInBaseUnits = BigNumber.from(amount).mul(assetPrice); + } almostEqual( totalCollateral, totalCollateralBefore.add(depositedAmountInBaseUnits) From 4b62fc9ea395bfd1e0b9b448e3c50bfa5fd843e1 Mon Sep 17 00:00:00 2001 From: GopherJ Date: Wed, 1 Feb 2023 16:55:45 +0800 Subject: [PATCH 11/19] chore: rename balancesOf to make it more clear Signed-off-by: GopherJ --- .../IAtomicCollateralizableERC721.sol | 2 +- .../protocol/libraries/logic/GenericLogic.sol | 2 +- .../base/MintableIncentivizedERC721.sol | 2 +- test/_xtoken_ntoken.spec.ts | 74 ++++++++++++++++++- 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/contracts/interfaces/IAtomicCollateralizableERC721.sol b/contracts/interfaces/IAtomicCollateralizableERC721.sol index d3dfb5165..fa3205e00 100644 --- a/contracts/interfaces/IAtomicCollateralizableERC721.sol +++ b/contracts/interfaces/IAtomicCollateralizableERC721.sol @@ -23,7 +23,7 @@ interface IAtomicCollateralizableERC721 { /** * @dev get the token balance of a specific user */ - function balancesOf(address user) + function underlyingBalancesOf(address user) external view returns ( diff --git a/contracts/protocol/libraries/logic/GenericLogic.sol b/contracts/protocol/libraries/logic/GenericLogic.sol index 18b826f5c..a86d8631a 100644 --- a/contracts/protocol/libraries/logic/GenericLogic.sol +++ b/contracts/protocol/libraries/logic/GenericLogic.sol @@ -375,7 +375,7 @@ library GenericLogic { uint256 atomicBalance, uint256 collateralizedBalance, - ) = IAtomicCollateralizableERC721(vars.xTokenAddress).balancesOf( + ) = IAtomicCollateralizableERC721(vars.xTokenAddress).underlyingBalancesOf( params.user ); totalValue = collateralizedBalance * assetPrice; diff --git a/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol b/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol index f4d07cf75..bfacbcb66 100644 --- a/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol +++ b/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol @@ -116,7 +116,7 @@ abstract contract MintableIncentivizedERC721 is _ERC721Data.userState[account].atomicBalance; } - function balancesOf(address account) + function underlyingBalancesOf(address account) public view virtual diff --git a/test/_xtoken_ntoken.spec.ts b/test/_xtoken_ntoken.spec.ts index a134fee28..eda34002b 100644 --- a/test/_xtoken_ntoken.spec.ts +++ b/test/_xtoken_ntoken.spec.ts @@ -3,7 +3,7 @@ import {expect} from "chai"; import {HALF_WAD} from "../helpers/constants"; import {waitForTx} from "../helpers/misc-utils"; import {testEnvFixture} from "./helpers/setup-env"; -import {supplyAndValidate} from "./helpers/validated-steps"; +import {mintAndValidate, supplyAndValidate} from "./helpers/validated-steps"; describe("NToken general", async () => { it("TC-ntoken-01: NToken is ERC721 compatible", async () => { @@ -125,4 +125,76 @@ describe("NToken general", async () => { 0 ); }); + + it("TC-ntoken-06: NToken atomic balance is correct when burn", async () => { + const { + nBAYC, + bayc, + pool, + users: [user1], + poolAdmin, + } = await loadFixture(testEnvFixture); + await waitForTx( + await nBAYC + .connect(poolAdmin.signer) + .setTraitsMultipliers(["0"], [HALF_WAD]) + ); + expect(await nBAYC.getTraitMultiplier("0")).eq(HALF_WAD); + expect(await nBAYC.isAtomicToken("0")).to.true; + + await supplyAndValidate(bayc, "1", user1, true); + + await waitForTx( + await pool + .connect(user1.signer) + .withdrawERC721(bayc.address, ["0"], user1.address) + ); + + expect(await nBAYC.atomicBalanceOf(user1.address)).to.be.eq(0); + expect(await nBAYC.atomicCollateralizedBalanceOf(user1.address)).to.be.eq( + 0 + ); + }); + + it("TC-ntoken-07: NToken atomic balance when mixed tokens mint", async () => { + const { + nBAYC, + bayc, + pool, + users: [user1], + poolAdmin, + } = await loadFixture(testEnvFixture); + await waitForTx( + await nBAYC + .connect(poolAdmin.signer) + .setTraitsMultipliers(["0"], [HALF_WAD]) + ); + expect(await nBAYC.getTraitMultiplier("0")).eq(HALF_WAD); + expect(await nBAYC.isAtomicToken("0")).to.true; + + await mintAndValidate(bayc, "2", user1); + + await waitForTx( + await bayc.connect(user1.signer).setApprovalForAll(pool.address, true) + ); + + await waitForTx( + await pool.connect(user1.signer).supplyERC721( + bayc.address, + [ + {tokenId: 0, useAsCollateral: true}, + {tokenId: 1, useAsCollateral: false}, + ], + user1.address, + "0" + ) + ); + + expect(await nBAYC.atomicBalanceOf(user1.address)).to.be.eq(1); + expect(await nBAYC.balanceOf(user1.address)).to.be.eq(2); + expect(await nBAYC.atomicCollateralizedBalanceOf(user1.address)).to.be.eq( + 1 + ); + expect(await nBAYC.collateralizedBalanceOf(user1.address)).to.be.eq(1); + }); }); From c481420c3c03792b1c1fbabce8c7184f0af07f3a Mon Sep 17 00:00:00 2001 From: GopherJ Date: Wed, 1 Feb 2023 18:01:49 +0800 Subject: [PATCH 12/19] fix: lint Signed-off-by: GopherJ --- .husky/pre-push | 14 +-- .../protocol/libraries/logic/GenericLogic.sol | 5 +- .../base/MintableIncentivizedERC721.sol | 1 - test/_xtoken_ntoken.spec.ts | 93 ++++++++++++++++++- 4 files changed, 95 insertions(+), 18 deletions(-) diff --git a/.husky/pre-push b/.husky/pre-push index 8b7727e59..1af03a08e 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,18 +1,6 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -contracts=$( (git diff --cached --name-only --diff-filter=ACMR | grep -Ei "\.sol$") || true) -scripts=$( (git diff --cached --name-only --diff-filter=ACMR | grep -Ei "\.ts$") || true) -if [ -z "${contracts}" ] && [ -z "${scripts}" ]; then - exit 0 -fi make format -if [ ! -z "${contracts}" ]; then - yarn typechain - git add $(echo "$contracts" | paste -s -d " " -) -fi - +yarn typechain yarn lint -if [ ! -z "${scripts}" ]; then - git add $(echo "$scripts" | paste -s -d " " -) -fi diff --git a/contracts/protocol/libraries/logic/GenericLogic.sol b/contracts/protocol/libraries/logic/GenericLogic.sol index a86d8631a..daeac49a2 100644 --- a/contracts/protocol/libraries/logic/GenericLogic.sol +++ b/contracts/protocol/libraries/logic/GenericLogic.sol @@ -375,9 +375,8 @@ library GenericLogic { uint256 atomicBalance, uint256 collateralizedBalance, - ) = IAtomicCollateralizableERC721(vars.xTokenAddress).underlyingBalancesOf( - params.user - ); + ) = IAtomicCollateralizableERC721(vars.xTokenAddress) + .underlyingBalancesOf(params.user); totalValue = collateralizedBalance * assetPrice; for (uint256 index = 0; index < atomicBalance; index++) { diff --git a/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol b/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol index bfacbcb66..57f339e7a 100644 --- a/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol +++ b/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol @@ -120,7 +120,6 @@ abstract contract MintableIncentivizedERC721 is public view virtual - override returns ( uint256, uint256, diff --git a/test/_xtoken_ntoken.spec.ts b/test/_xtoken_ntoken.spec.ts index eda34002b..9bbefdebb 100644 --- a/test/_xtoken_ntoken.spec.ts +++ b/test/_xtoken_ntoken.spec.ts @@ -1,7 +1,9 @@ import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; import {expect} from "chai"; -import {HALF_WAD} from "../helpers/constants"; +import {BigNumber} from "ethers"; +import {HALF_WAD, WAD} from "../helpers/constants"; import {waitForTx} from "../helpers/misc-utils"; +import {ProtocolErrors} from "../helpers/types"; import {testEnvFixture} from "./helpers/setup-env"; import {mintAndValidate, supplyAndValidate} from "./helpers/validated-steps"; @@ -197,4 +199,93 @@ describe("NToken general", async () => { ); expect(await nBAYC.collateralizedBalanceOf(user1.address)).to.be.eq(1); }); + + it("TC-ntoken-08: only pool admin is allowed to set trait multipliers", async () => { + const { + nBAYC, + users: [user1], + poolAdmin, + } = await loadFixture(testEnvFixture); + await expect( + nBAYC.connect(user1.signer).setTraitsMultipliers(["0"], [HALF_WAD]) + ).to.be.revertedWith(ProtocolErrors.CALLER_NOT_POOL_ADMIN); + await expect( + nBAYC.connect(poolAdmin.signer).setTraitsMultipliers(["0"], [HALF_WAD]) + ); + }); + + it("TC-ntoken-09: trait multipliers must be in range [0, 10)", async () => { + const {nBAYC, poolAdmin} = await loadFixture(testEnvFixture); + await waitForTx( + await nBAYC.connect(poolAdmin.signer).setTraitsMultipliers(["0"], ["0"]) + ); + await waitForTx( + await nBAYC + .connect(poolAdmin.signer) + .setTraitsMultipliers(["0"], [HALF_WAD]) + ); + await expect( + nBAYC + .connect(poolAdmin.signer) + .setTraitsMultipliers(["0"], [BigNumber.from(WAD).mul(10)]) + ).to.be.revertedWith(ProtocolErrors.INVALID_AMOUNT); + await waitForTx( + await nBAYC + .connect(poolAdmin.signer) + .setTraitsMultipliers(["0"], [BigNumber.from(WAD).mul(10).sub(1)]) + ); + }); + + it("TC-ntoken-10: non-atomic tokens have no limit but atomic tokens do have", async () => { + const { + nBAYC, + bayc, + poolAdmin, + pool, + users: [user1], + } = await loadFixture(testEnvFixture); + await waitForTx(await nBAYC.connect(poolAdmin.signer).setBalanceLimit(1)); + + await mintAndValidate(bayc, "12", user1); + + await waitForTx( + await bayc.connect(user1.signer).setApprovalForAll(pool.address, true) + ); + + await waitForTx( + await pool.connect(user1.signer).supplyERC721( + bayc.address, + [...Array(10).keys()].map((x) => ({ + tokenId: x, + useAsCollateral: true, + })), + user1.address, + "0" + ) + ); + + await waitForTx( + await nBAYC + .connect(poolAdmin.signer) + .setTraitsMultipliers(["10", "11"], [HALF_WAD, HALF_WAD]) + ); + + await expect( + pool.connect(user1.signer).supplyERC721( + bayc.address, + [ + { + tokenId: "10", + useAsCollateral: true, + }, + { + tokenId: "11", + useAsCollateral: true, + }, + ], + user1.address, + "0" + ) + ).to.revertedWith(ProtocolErrors.NTOKEN_BALANCE_EXCEEDED); + }); }); From bb0705dc795b1efb8ea250eca761d393b330add9 Mon Sep 17 00:00:00 2001 From: GopherJ Date: Wed, 1 Feb 2023 18:03:16 +0800 Subject: [PATCH 13/19] chore: dont format in during push Signed-off-by: GopherJ --- .husky/pre-push | 1 - 1 file changed, 1 deletion(-) diff --git a/.husky/pre-push b/.husky/pre-push index 1af03a08e..2ec3c4c1f 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,6 +1,5 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -make format yarn typechain yarn lint From fabf472a073e341bf557f58bb3484158504c5169 Mon Sep 17 00:00:00 2001 From: GopherJ Date: Wed, 1 Feb 2023 19:34:17 +0800 Subject: [PATCH 14/19] chore: add simple liquidation test Signed-off-by: GopherJ --- ...ol_core_erc721_auction_liquidation.spec.ts | 63 ++++++++- test/_xtoken_ntoken.spec.ts | 125 +++++++++++++++++- 2 files changed, 185 insertions(+), 3 deletions(-) diff --git a/test/_pool_core_erc721_auction_liquidation.spec.ts b/test/_pool_core_erc721_auction_liquidation.spec.ts index 154ca76c6..6ad31c530 100644 --- a/test/_pool_core_erc721_auction_liquidation.spec.ts +++ b/test/_pool_core_erc721_auction_liquidation.spec.ts @@ -2,7 +2,7 @@ import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; import {expect} from "chai"; import {BigNumber} from "ethers"; import {parseEther} from "ethers/lib/utils"; -import {ZERO_ADDRESS} from "../helpers/constants"; +import {WAD, ZERO_ADDRESS} from "../helpers/constants"; import {getAggregator} from "../helpers/contracts-getters"; import {advanceBlock, waitForTx} from "../helpers/misc-utils"; import {ProtocolErrors} from "../helpers/types"; @@ -945,5 +945,66 @@ describe("Liquidation Auction", () => { expect(await nBAYC.isAuctioned(0)).to.be.false; }); + + it("TC-auction-liquidation-33 Trait multiplier is configured on the token. Auction price * trait multiplier is used in liquidation, not floor or atomic price", async () => { + const { + users: [borrower, liquidator], + pool, + bayc, + nBAYC, + poolAdmin, + } = testEnv; + + await waitForTx( + await nBAYC + .connect(poolAdmin.signer) + .setTraitsMultipliers(["0"], [BigNumber.from(WAD).mul(2)]) + ); + + // drop BAYC price to liquidation levels + await changePriceAndValidate(bayc, "4"); + + await waitForTx( + await pool + .connect(liquidator.signer) + .startAuction(borrower.address, bayc.address, 0) + ); + + const {startTime, tickLength} = await pool.getAuctionData( + nBAYC.address, + 0 + ); + + // prices drops to ~1.5 floor price + await advanceBlock( + startTime.add(tickLength.mul(BigNumber.from(30))).toNumber() + ); + + // 4 * 2 * 1.5 / 1.05 = 11.428571428571428571 needed + await expect( + pool + .connect(liquidator.signer) + .liquidateERC721( + bayc.address, + borrower.address, + 0, + parseEther("11").toString(), + false, + {gasLimit: 5000000} + ) + ).to.be.reverted; + + await pool + .connect(liquidator.signer) + .liquidateERC721( + bayc.address, + borrower.address, + 0, + parseEther("12").toString(), + false, + {gasLimit: 5000000} + ); + expect(await nBAYC.balanceOf(borrower.address)).to.be.equal(2); + }); }); }); diff --git a/test/_xtoken_ntoken.spec.ts b/test/_xtoken_ntoken.spec.ts index 9bbefdebb..841981599 100644 --- a/test/_xtoken_ntoken.spec.ts +++ b/test/_xtoken_ntoken.spec.ts @@ -60,19 +60,30 @@ describe("NToken general", async () => { await supplyAndValidate(bayc, "1", user1, true); + // remove from collateral await waitForTx( await pool .connect(user1.signer) .setUserUseERC721AsCollateral(bayc.address, ["0"], false) ); - expect(await nBAYC.atomicBalanceOf(user1.address)).to.be.eq(1); expect(await nBAYC.atomicCollateralizedBalanceOf(user1.address)).to.be.eq( 0 ); + + // add back to collateral + await waitForTx( + await pool + .connect(user1.signer) + .setUserUseERC721AsCollateral(bayc.address, ["0"], true) + ); + expect(await nBAYC.atomicBalanceOf(user1.address)).to.be.eq(1); + expect(await nBAYC.atomicCollateralizedBalanceOf(user1.address)).to.be.eq( + 1 + ); }); - it("TC-ntoken-04: NToken atomic balance is correct when trait multiplier got removed", async () => { + it("TC-ntoken-04: NToken atomic balance is correct when trait multiplier got removed or added", async () => { const { nBAYC, bayc, @@ -89,6 +100,7 @@ describe("NToken general", async () => { await supplyAndValidate(bayc, "1", user1, true); + // remove multiplier await waitForTx( await nBAYC.connect(poolAdmin.signer).setTraitsMultipliers(["0"], ["0"]) ); @@ -97,6 +109,17 @@ describe("NToken general", async () => { expect(await nBAYC.atomicCollateralizedBalanceOf(user1.address)).to.be.eq( 0 ); + + // add back multiplier + await waitForTx( + await nBAYC + .connect(poolAdmin.signer) + .setTraitsMultipliers(["0"], [HALF_WAD]) + ); + expect(await nBAYC.atomicBalanceOf(user1.address)).to.be.eq(1); + expect(await nBAYC.atomicCollateralizedBalanceOf(user1.address)).to.be.eq( + 1 + ); }); it("TC-ntoken-05: NToken atomic balance is correct when transfer", async () => { @@ -126,6 +149,11 @@ describe("NToken general", async () => { expect(await nBAYC.atomicCollateralizedBalanceOf(user2.address)).to.be.eq( 0 ); + + expect(await nBAYC.atomicBalanceOf(user1.address)).to.be.eq(0); + expect(await nBAYC.atomicCollateralizedBalanceOf(user1.address)).to.be.eq( + 0 + ); }); it("TC-ntoken-06: NToken atomic balance is correct when burn", async () => { @@ -288,4 +316,97 @@ describe("NToken general", async () => { ) ).to.revertedWith(ProtocolErrors.NTOKEN_BALANCE_EXCEEDED); }); + + it("TC-ntoken-11: userAccountData increases multiplier times when there is a multiplier", async () => { + const { + nBAYC, + bayc, + poolAdmin, + pool, + users: [user1], + } = await loadFixture(testEnvFixture); + await mintAndValidate(bayc, "1", user1); + + await waitForTx( + await bayc.connect(user1.signer).setApprovalForAll(pool.address, true) + ); + + // somehow hardhat may didn't restore snapshot correctly etc + await waitForTx( + await nBAYC.connect(poolAdmin.signer).setTraitsMultipliers(["0"], ["0"]) + ); + await waitForTx( + await pool.connect(user1.signer).supplyERC721( + bayc.address, + [ + { + tokenId: 0, + useAsCollateral: true, + }, + ], + user1.address, + "0" + ) + ); + + const accountDataBefore = await pool.getUserAccountData(user1.address); + await waitForTx( + await nBAYC + .connect(poolAdmin.signer) + .setTraitsMultipliers(["0"], [BigNumber.from(WAD).mul(2)]) + ); + const accountDataAfter = await pool.getUserAccountData(user1.address); + + expect(accountDataAfter.totalCollateralBase).to.be.eq( + accountDataBefore.totalCollateralBase.mul(2) + ); + }); + + it("TC-ntoken-12: uniswap cannot have trait multiplier", async () => { + const {nUniswapV3, poolAdmin} = await loadFixture(testEnvFixture); + await expect( + nUniswapV3 + .connect(poolAdmin.signer) + .setTraitsMultipliers(["0"], [HALF_WAD]) + ).to.be.reverted; + }); + + it("TC-ntoken-13: atomicTokenOfOwnerByIndex works as expected", async () => { + const { + nBAYC, + bayc, + poolAdmin, + pool, + users: [user1], + } = await loadFixture(testEnvFixture); + await mintAndValidate(bayc, "3", user1); + + await waitForTx( + await bayc.connect(user1.signer).setApprovalForAll(pool.address, true) + ); + + await waitForTx( + await nBAYC + .connect(poolAdmin.signer) + .setTraitsMultipliers(["0", "1", "2"], [HALF_WAD, HALF_WAD, HALF_WAD]) + ); + + await waitForTx( + await pool.connect(user1.signer).supplyERC721( + bayc.address, + [...Array(3).keys()] + .map((x) => ({ + tokenId: x, + useAsCollateral: true, + })) + .reverse(), + user1.address, + "0" + ) + ); + + expect(await nBAYC.atomicTokenOfOwnerByIndex(user1.address, 0)).eq(2); + expect(await nBAYC.atomicTokenOfOwnerByIndex(user1.address, 1)).eq(1); + expect(await nBAYC.atomicTokenOfOwnerByIndex(user1.address, 2)).eq(0); + }); }); From a8a0964a9f44aeff068dbee68bc396a4546b2243 Mon Sep 17 00:00:00 2001 From: GopherJ Date: Wed, 1 Feb 2023 19:44:55 +0800 Subject: [PATCH 15/19] chore: simplify Signed-off-by: GopherJ --- .../tokenization/libraries/MintableERC721Logic.sol | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol b/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol index 8c8c88c48..e41b43496 100644 --- a/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol +++ b/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol @@ -282,14 +282,9 @@ library MintableERC721Logic { erc721Data, to, tokenId, - isAtomic ? vars.atomicBalance : vars.balance, + isAtomic ? vars.atomicBalance++ : vars.balance++, isAtomic ); - if (isAtomic) { - vars.atomicBalance += 1; - } else { - vars.balance += 1; - } erc721Data.owners[tokenId] = to; @@ -375,14 +370,9 @@ library MintableERC721Logic { erc721Data, user, tokenId, - isAtomic ? vars.atomicBalance : vars.balance, + isAtomic ? vars.atomicBalance-- : vars.balance--, isAtomic ); - if (isAtomic) { - vars.atomicBalance -= 1; - } else { - vars.balance -= 1; - } // Clear approvals _approve(erc721Data, address(0), tokenId); From d9249ea6e867dde67f8115dec2cd8ecaf9db6c39 Mon Sep 17 00:00:00 2001 From: GopherJ Date: Wed, 1 Feb 2023 21:36:59 +0800 Subject: [PATCH 16/19] feat: improve tokenPrice calc Signed-off-by: GopherJ --- test/_xtoken_ntoken.spec.ts | 46 +++++++++++++++++++++++++++++++++ test/helpers/validated-steps.ts | 32 +++++++++++++++-------- 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/test/_xtoken_ntoken.spec.ts b/test/_xtoken_ntoken.spec.ts index 841981599..b675dbb0a 100644 --- a/test/_xtoken_ntoken.spec.ts +++ b/test/_xtoken_ntoken.spec.ts @@ -409,4 +409,50 @@ describe("NToken general", async () => { expect(await nBAYC.atomicTokenOfOwnerByIndex(user1.address, 1)).eq(1); expect(await nBAYC.atomicTokenOfOwnerByIndex(user1.address, 2)).eq(0); }); + + it("TC-ntoken-14: tokenOfOwnerByIndex works as expected", async () => { + const { + nBAYC, + bayc, + poolAdmin, + pool, + users: [user1], + } = await loadFixture(testEnvFixture); + await mintAndValidate(bayc, "4", user1); + + await waitForTx( + await bayc.connect(user1.signer).setApprovalForAll(pool.address, true) + ); + + await waitForTx( + await nBAYC + .connect(poolAdmin.signer) + .setTraitsMultipliers( + ["0", "1", "2", "3"], + ["0", "0", HALF_WAD, HALF_WAD] + ) + ); + + await waitForTx( + await pool.connect(user1.signer).supplyERC721( + bayc.address, + [...Array(4).keys()] + .map((x) => ({ + tokenId: x, + useAsCollateral: true, + })) + .reverse(), + user1.address, + "0" + ) + ); + + // Tokens: [1, 0] + // AtomicTokens: [3, 2] + + expect(await nBAYC.tokenOfOwnerByIndex(user1.address, 0)).eq(1); + expect(await nBAYC.tokenOfOwnerByIndex(user1.address, 1)).eq(0); + expect(await nBAYC.tokenOfOwnerByIndex(user1.address, 2)).eq(3); + expect(await nBAYC.tokenOfOwnerByIndex(user1.address, 3)).eq(2); + }); }); diff --git a/test/helpers/validated-steps.ts b/test/helpers/validated-steps.ts index efacf9de4..10d97f296 100644 --- a/test/helpers/validated-steps.ts +++ b/test/helpers/validated-steps.ts @@ -158,17 +158,18 @@ export const supplyAndValidate = async ( // asset is used as collateral, so total collateral increases in supplied amount const totalCollateral = (await pool.getUserAccountData(user.address)) .totalCollateralBase; - let depositedAmountInBaseUnits; + let depositedAmountInBaseUnits = BigNumber.from("0"); if (isNFT && nftIdsToUse) { - const multiplier = await ( - await getNToken(xTokenAddress) - ).getTraitMultiplier(nftIdsToUse[0]); - if (!multiplier.eq(0) && !multiplier.eq(WAD)) { - depositedAmountInBaseUnits = BigNumber.from(amount) - .mul(assetPrice) - .wadMul(multiplier); - } else { - depositedAmountInBaseUnits = BigNumber.from(amount).mul(assetPrice); + const nToken = await getNToken(xTokenAddress); + for (const nftId of nftIdsToUse) { + const multiplier = await nToken.getTraitMultiplier(nftId); + if (!multiplier.eq(0) && !multiplier.eq(WAD)) { + depositedAmountInBaseUnits = depositedAmountInBaseUnits.add( + assetPrice.wadMul(multiplier) + ); + } else { + depositedAmountInBaseUnits = depositedAmountInBaseUnits.add(assetPrice); + } } } else { depositedAmountInBaseUnits = BigNumber.from(amount).mul(assetPrice); @@ -1041,11 +1042,20 @@ const fetchLiquidationData = async ( if (isAuctioned) { currentPriceMultiplier = auctionData.currentPriceMultiplier; } + const traitMultiplier = await ( + collateralXToken as NToken + ).getTraitMultiplier(nftId); + if (!traitMultiplier.eq(0) && !traitMultiplier.eq(WAD)) { + currentPriceMultiplier = currentPriceMultiplier.wadMul(traitMultiplier); + } } const collateralAssetPrice = nftId != undefined && (await collateralXToken.getXTokenType()) == XTokenType.NTokenUniswapV3 - ? await (await getUniswapV3OracleWrapper()).getTokenPrice(nftId as number) + ? await paraSpaceOracle.getTokenPrice( + collateralXTokenAddress, + nftId as number + ) : await paraSpaceOracle.getAssetPrice(collateralToken.address); const collateralAssetAuctionPrice = collateralAssetPrice.wadMul( From 8ddd805240d0e7391a7872f0b8fb8625c1ddb18b Mon Sep 17 00:00:00 2001 From: GopherJ Date: Thu, 2 Feb 2023 10:12:02 +0800 Subject: [PATCH 17/19] feat: gas optimization Signed-off-by: GopherJ --- .../IAtomicCollateralizableERC721.sol | 8 +++++ .../protocol/libraries/logic/GenericLogic.sol | 19 ++--------- .../base/MintableIncentivizedERC721.sol | 32 +++++++++++++++++++ 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/contracts/interfaces/IAtomicCollateralizableERC721.sol b/contracts/interfaces/IAtomicCollateralizableERC721.sol index fa3205e00..32fc28143 100644 --- a/contracts/interfaces/IAtomicCollateralizableERC721.sol +++ b/contracts/interfaces/IAtomicCollateralizableERC721.sol @@ -58,4 +58,12 @@ interface IAtomicCollateralizableERC721 { external view returns (uint256); + + /** + * @dev get the trait multiplier sum of all collateralized tokens + */ + function getTraitMultiplierSumOfAllCollateralized(address user) + external + view + returns (uint256); } diff --git a/contracts/protocol/libraries/logic/GenericLogic.sol b/contracts/protocol/libraries/logic/GenericLogic.sol index daeac49a2..ba3a20c5c 100644 --- a/contracts/protocol/libraries/logic/GenericLogic.sol +++ b/contracts/protocol/libraries/logic/GenericLogic.sol @@ -378,22 +378,9 @@ library GenericLogic { ) = IAtomicCollateralizableERC721(vars.xTokenAddress) .underlyingBalancesOf(params.user); totalValue = collateralizedBalance * assetPrice; - - for (uint256 index = 0; index < atomicBalance; index++) { - uint256 tokenId = IAtomicCollateralizableERC721(vars.xTokenAddress) - .atomicTokenOfOwnerByIndex(params.user, index); - if ( - ICollateralizableERC721(vars.xTokenAddress).isUsedAsCollateral( - tokenId - ) - ) { - totalValue += Helpers.getTraitBoostedTokenPrice( - vars.xTokenAddress, - assetPrice, - tokenId - ); - } - } + totalValue += IAtomicCollateralizableERC721(vars.xTokenAddress) + .getTraitMultiplierSumOfAllCollateralized(params.user) + .wadMul(assetPrice); } function getLtvAndLTForUniswapV3( diff --git a/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol b/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol index 57f339e7a..601a5f5f4 100644 --- a/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol +++ b/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol @@ -22,6 +22,7 @@ import {IACLManager} from "../../../interfaces/IACLManager.sol"; import {DataTypes} from "../../libraries/types/DataTypes.sol"; import {ReentrancyGuard} from "../../../dependencies/openzeppelin/contracts/ReentrancyGuard.sol"; import {MintableERC721Logic, UserState, MintableERC721Data} from "../libraries/MintableERC721Logic.sol"; +import {Helpers} from "../../libraries/helpers/Helpers.sol"; /** * @title MintableIncentivizedERC721 @@ -605,6 +606,37 @@ abstract contract MintableIncentivizedERC721 is return _ERC721Data.traitsMultipliers[tokenId]; } + /// @inheritdoc IAtomicCollateralizableERC721 + function getTraitMultiplierSumOfAllCollateralized(address user) + external + view + returns (uint256 totalMultiplier) + { + uint256 atomicCollateralizedBalance = _ERC721Data + .userState[user] + .atomicCollateralizedBalance; + uint256 atomicBalance = _ERC721Data.userState[user].atomicBalance; + + uint256 collateralizedTokens = 0; + for ( + uint256 index = 0; + index < atomicBalance && + collateralizedTokens != atomicCollateralizedBalance; + index++ + ) { + uint256 tokenId = _ERC721Data.ownedAtomicTokens[user][index]; + uint256 multiplier = _ERC721Data.traitsMultipliers[tokenId]; + if (_ERC721Data.isUsedAsCollateral[tokenId]) { + if (Helpers.isTraitMultiplierEffective(multiplier)) { + totalMultiplier += multiplier; + } else { + totalMultiplier += WadRayMath.WAD; + } + collateralizedTokens++; + } + } + } + /// @inheritdoc IAuctionableERC721 function startAuction(uint256 tokenId) external From ae058f8c424257258ee2d64458a72ea19956d26d Mon Sep 17 00:00:00 2001 From: GopherJ Date: Thu, 2 Feb 2023 10:45:01 +0800 Subject: [PATCH 18/19] feat: remove redundant check Signed-off-by: GopherJ --- .../tokenization/base/MintableIncentivizedERC721.sol | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol b/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol index 601a5f5f4..e9480fb0c 100644 --- a/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol +++ b/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol @@ -615,13 +615,11 @@ abstract contract MintableIncentivizedERC721 is uint256 atomicCollateralizedBalance = _ERC721Data .userState[user] .atomicCollateralizedBalance; - uint256 atomicBalance = _ERC721Data.userState[user].atomicBalance; - uint256 collateralizedTokens = 0; + uint256 collateralizedTokens; for ( uint256 index = 0; - index < atomicBalance && - collateralizedTokens != atomicCollateralizedBalance; + collateralizedTokens != atomicCollateralizedBalance; index++ ) { uint256 tokenId = _ERC721Data.ownedAtomicTokens[user][index]; From e9c2b17951b13cce07795776aad29a26e30e80aa Mon Sep 17 00:00:00 2001 From: GopherJ Date: Thu, 2 Feb 2023 14:12:51 +0800 Subject: [PATCH 19/19] fix: underlyingAsset slot Signed-off-by: GopherJ --- contracts/protocol/tokenization/NToken.sol | 14 +++++------ .../tokenization/NTokenApeStaking.sol | 6 ++--- .../protocol/tokenization/NTokenMoonBirds.sol | 24 +++++++++++-------- .../protocol/tokenization/NTokenUniswapV3.sol | 14 ++++++----- .../base/MintableIncentivizedERC721.sol | 2 -- .../libraries/MintableERC721Logic.sol | 1 + 6 files changed, 33 insertions(+), 28 deletions(-) diff --git a/contracts/protocol/tokenization/NToken.sol b/contracts/protocol/tokenization/NToken.sol index 0ff3f812e..623c11b20 100644 --- a/contracts/protocol/tokenization/NToken.sol +++ b/contracts/protocol/tokenization/NToken.sol @@ -62,7 +62,7 @@ contract NToken is VersionedInitializable, MintableIncentivizedERC721, INToken { _setSymbol(nTokenSymbol); require(underlyingAsset != address(0), Errors.ZERO_ADDRESS_NOT_VALID); - _underlyingAsset = underlyingAsset; + _ERC721Data.underlyingAsset = underlyingAsset; _ERC721Data.rewardController = incentivesController; emit Initialized( @@ -104,7 +104,7 @@ contract NToken is VersionedInitializable, MintableIncentivizedERC721, INToken { if (receiverOfUnderlying != address(this)) { for (uint256 index = 0; index < tokenIds.length; index++) { - IERC721(_underlyingAsset).safeTransferFrom( + IERC721(_ERC721Data.underlyingAsset).safeTransferFrom( address(this), receiverOfUnderlying, tokenIds[index] @@ -139,7 +139,7 @@ contract NToken is VersionedInitializable, MintableIncentivizedERC721, INToken { uint256[] calldata ids ) external override onlyPoolAdmin { require( - token != _underlyingAsset, + token != _ERC721Data.underlyingAsset, Errors.UNDERLYING_ASSET_CAN_NOT_BE_TRANSFERRED ); for (uint256 i = 0; i < ids.length; i++) { @@ -192,7 +192,7 @@ contract NToken is VersionedInitializable, MintableIncentivizedERC721, INToken { override returns (address) { - return _underlyingAsset; + return _ERC721Data.underlyingAsset; } /// @inheritdoc INToken @@ -203,7 +203,7 @@ contract NToken is VersionedInitializable, MintableIncentivizedERC721, INToken { onlyPool nonReentrant { - IERC721(_underlyingAsset).safeTransferFrom( + IERC721(_ERC721Data.underlyingAsset).safeTransferFrom( address(this), target, tokenId @@ -235,7 +235,7 @@ contract NToken is VersionedInitializable, MintableIncentivizedERC721, INToken { uint256 tokenId, bool validate ) internal virtual { - address underlyingAsset = _underlyingAsset; + address underlyingAsset = _ERC721Data.underlyingAsset; uint256 fromBalanceBefore; if (validate) { @@ -318,7 +318,7 @@ contract NToken is VersionedInitializable, MintableIncentivizedERC721, INToken { override returns (string memory) { - return IERC721Metadata(_underlyingAsset).tokenURI(tokenId); + return IERC721Metadata(_ERC721Data.underlyingAsset).tokenURI(tokenId); } function getXTokenType() diff --git a/contracts/protocol/tokenization/NTokenApeStaking.sol b/contracts/protocol/tokenization/NTokenApeStaking.sol index 37ed58c8c..17f79a259 100644 --- a/contracts/protocol/tokenization/NTokenApeStaking.sol +++ b/contracts/protocol/tokenization/NTokenApeStaking.sol @@ -94,7 +94,7 @@ abstract contract NTokenApeStaking is NToken, INTokenApeStaking { ApeStakingLogic.UnstakeAndRepayParams({ POOL: POOL, _apeCoinStaking: _apeCoinStaking, - _underlyingAsset: _underlyingAsset, + _underlyingAsset: _ERC721Data.underlyingAsset, poolId: POOL_ID(), tokenId: tokenId, incentiveReceiver: address(0), @@ -119,7 +119,7 @@ abstract contract NTokenApeStaking is NToken, INTokenApeStaking { ApeStakingLogic.UnstakeAndRepayParams({ POOL: POOL, _apeCoinStaking: _apeCoinStaking, - _underlyingAsset: _underlyingAsset, + _underlyingAsset: _ERC721Data.underlyingAsset, poolId: POOL_ID(), tokenId: tokenIds[index], incentiveReceiver: address(0), @@ -183,7 +183,7 @@ abstract contract NTokenApeStaking is NToken, INTokenApeStaking { ApeStakingLogic.UnstakeAndRepayParams({ POOL: POOL, _apeCoinStaking: _apeCoinStaking, - _underlyingAsset: _underlyingAsset, + _underlyingAsset: _ERC721Data.underlyingAsset, poolId: POOL_ID(), tokenId: tokenId, incentiveReceiver: incentiveReceiver, diff --git a/contracts/protocol/tokenization/NTokenMoonBirds.sol b/contracts/protocol/tokenization/NTokenMoonBirds.sol index 52ca79710..dd952ef6e 100644 --- a/contracts/protocol/tokenization/NTokenMoonBirds.sol +++ b/contracts/protocol/tokenization/NTokenMoonBirds.sol @@ -49,11 +49,11 @@ contract NTokenMoonBirds is NToken, IMoonBirdBase { if (receiverOfUnderlying != address(this)) { for (uint256 index = 0; index < tokenIds.length; index++) { - IMoonBird(_underlyingAsset).safeTransferWhileNesting( - address(this), - receiverOfUnderlying, - tokenIds[index] - ); + IMoonBird(_ERC721Data.underlyingAsset).safeTransferWhileNesting( + address(this), + receiverOfUnderlying, + tokenIds[index] + ); } } @@ -72,7 +72,7 @@ contract NTokenMoonBirds is NToken, IMoonBirdBase { return this.onERC721Received.selector; } - if (msg.sender == _underlyingAsset) { + if (msg.sender == _ERC721Data.underlyingAsset) { // supply the received token to the pool and set it as collateral DataTypes.ERC721SupplyParams[] memory tokenData = new DataTypes.ERC721SupplyParams[](1); @@ -82,7 +82,11 @@ contract NTokenMoonBirds is NToken, IMoonBirdBase { useAsCollateral: true }); - POOL.supplyERC721FromNToken(_underlyingAsset, tokenData, from); + POOL.supplyERC721FromNToken( + _ERC721Data.underlyingAsset, + tokenData, + from + ); } return this.onERC721Received.selector; @@ -100,7 +104,7 @@ contract NTokenMoonBirds is NToken, IMoonBirdBase { ); } - IMoonBird(_underlyingAsset).toggleNesting(tokenIds); + IMoonBird(_ERC721Data.underlyingAsset).toggleNesting(tokenIds); } /** @@ -116,7 +120,7 @@ contract NTokenMoonBirds is NToken, IMoonBirdBase { uint256 total ) { - return IMoonBird(_underlyingAsset).nestingPeriod(tokenId); + return IMoonBird(_ERC721Data.underlyingAsset).nestingPeriod(tokenId); } /** @@ -124,6 +128,6 @@ contract NTokenMoonBirds is NToken, IMoonBirdBase { This function check if nesting is open for the underlying tokens */ function nestingOpen() external view returns (bool) { - return IMoonBird(_underlyingAsset).nestingOpen(); + return IMoonBird(_ERC721Data.underlyingAsset).nestingOpen(); } } diff --git a/contracts/protocol/tokenization/NTokenUniswapV3.sol b/contracts/protocol/tokenization/NTokenUniswapV3.sol index 3a3aa3187..a915b33d2 100644 --- a/contracts/protocol/tokenization/NTokenUniswapV3.sol +++ b/contracts/protocol/tokenization/NTokenUniswapV3.sol @@ -69,9 +69,8 @@ contract NTokenUniswapV3 is NToken, INTokenUniswapV3 { deadline: block.timestamp }); - INonfungiblePositionManager(_underlyingAsset).decreaseLiquidity( - params - ); + INonfungiblePositionManager(_ERC721Data.underlyingAsset) + .decreaseLiquidity(params); } ( @@ -87,7 +86,9 @@ contract NTokenUniswapV3 is NToken, INTokenUniswapV3 { , , - ) = INonfungiblePositionManager(_underlyingAsset).positions(tokenId); + ) = INonfungiblePositionManager(_ERC721Data.underlyingAsset).positions( + tokenId + ); address weth = _addressesProvider.getWETH(); receiveEthAsWeth = (receiveEthAsWeth && @@ -101,8 +102,9 @@ contract NTokenUniswapV3 is NToken, INTokenUniswapV3 { amount1Max: type(uint128).max }); - (amount0, amount1) = INonfungiblePositionManager(_underlyingAsset) - .collect(collectParams); + (amount0, amount1) = INonfungiblePositionManager( + _ERC721Data.underlyingAsset + ).collect(collectParams); if (receiveEthAsWeth) { uint256 balanceWeth = IERC20(weth).balanceOf(address(this)); diff --git a/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol b/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol index e9480fb0c..0c3659be6 100644 --- a/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol +++ b/contracts/protocol/tokenization/base/MintableIncentivizedERC721.sol @@ -76,8 +76,6 @@ abstract contract MintableIncentivizedERC721 is IPool public immutable POOL; bool public immutable ATOMIC_PRICING; - address internal _underlyingAsset; - /** * @dev Constructor. * @param pool The reference to the main Pool contract diff --git a/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol b/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol index e41b43496..536958670 100644 --- a/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol +++ b/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol @@ -46,6 +46,7 @@ struct MintableERC721Data { uint64 balanceLimit; mapping(uint256 => bool) isUsedAsCollateral; mapping(uint256 => DataTypes.Auction) auctions; + address underlyingAsset; // Mapping from owner to list of owned atomic token IDs mapping(address => mapping(uint256 => uint256)) ownedAtomicTokens; // Mapping from token ID to index of the owned atomic tokens list