diff --git a/.husky/pre-push b/.husky/pre-push index 8b7727e59..2ec3c4c1f 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,18 +1,5 @@ #!/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/interfaces/IAtomicCollateralizableERC721.sol b/contracts/interfaces/IAtomicCollateralizableERC721.sol new file mode 100644 index 000000000..32fc28143 --- /dev/null +++ b/contracts/interfaces/IAtomicCollateralizableERC721.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +/** + * @title IAtomicCollateralizableERC721 + * @author Parallel + * @notice Defines the basic interface for an AtomicCollateralizableERC721. + **/ +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 get the token balance of a specific user + */ + function underlyingBalancesOf(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 + */ + function getTraitMultiplier(uint256 tokenId) + 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/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/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 19be4c206..ba3a20c5c 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"; @@ -15,7 +16,8 @@ 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"; +import {Helpers} from "../../libraries/helpers/Helpers.sol"; /** * @title GenericLogic library @@ -363,37 +365,22 @@ library GenericLogic { DataTypes.CalculateUserAccountDataParams memory params, 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 - ); - 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; - } + uint256 assetPrice = _getAssetPrice( + params.oracle, + vars.currentReserveAddress + ); + + ( + , + uint256 atomicBalance, + uint256 collateralizedBalance, + + ) = IAtomicCollateralizableERC721(vars.xTokenAddress) + .underlyingBalancesOf(params.user); + totalValue = collateralizedBalance * assetPrice; + totalValue += IAtomicCollateralizableERC721(vars.xTokenAddress) + .getTraitMultiplierSumOfAllCollateralized(params.user) + .wadMul(assetPrice); } function getLtvAndLTForUniswapV3( diff --git a/contracts/protocol/libraries/logic/LiquidationLogic.sol b/contracts/protocol/libraries/logic/LiquidationLogic.sol index 80a3482f5..1b7dbbc68 100644 --- a/contracts/protocol/libraries/logic/LiquidationLogic.sol +++ b/contracts/protocol/libraries/logic/LiquidationLogic.sol @@ -18,7 +18,9 @@ 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 {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"; @@ -40,6 +42,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,15 +779,24 @@ library LiquidationLogic { ).collateralizedBalanceOf(params.borrower); // price of the asset that is used as collateral - if (INToken(superVars.collateralXToken).getAtomicPricingConfig()) { + if ( + IXTokenType(superVars.collateralXToken).getXTokenType() == + XTokenType.NTokenUniswapV3 + ) { vars.collateralPrice = IPriceOracleGetter(params.priceOracle) .getTokenPrice( params.collateralAsset, params.collateralTokenId ); } 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 72ffaa911..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,11 +318,7 @@ contract NToken is VersionedInitializable, MintableIncentivizedERC721, INToken { override returns (string memory) { - return IERC721Metadata(_underlyingAsset).tokenURI(tokenId); - } - - function getAtomicPricingConfig() external view returns (bool) { - return ATOMIC_PRICING; + 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 cd4f52dad..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)); @@ -141,6 +143,15 @@ contract NTokenUniswapV3 is NToken, INTokenUniswapV3 { ); } + 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..0c3659be6 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"; @@ -21,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 @@ -30,6 +32,7 @@ import {MintableERC721Logic, UserState, MintableERC721Data} from "../libraries/M abstract contract MintableIncentivizedERC721 is ReentrancyGuard, ICollateralizableERC721, + IAtomicCollateralizableERC721, IAuctionableERC721, Context, IERC721Metadata, @@ -73,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 @@ -109,7 +110,28 @@ abstract contract MintableIncentivizedERC721 is override returns (uint256) { - return _ERC721Data.userState[account].balance; + return + _ERC721Data.userState[account].balance + + _ERC721Data.userState[account].atomicBalance; + } + + function underlyingBalancesOf(address account) + public + view + virtual + returns ( + uint256, + uint256, + uint256, + uint256 + ) + { + return ( + _ERC721Data.userState[account].balance, + _ERC721Data.userState[account].atomicBalance, + _ERC721Data.userState[account].collateralizedBalance, + _ERC721Data.userState[account].atomicCollateralizedBalance + ); } /** @@ -394,6 +416,7 @@ abstract contract MintableIncentivizedERC721 is MintableERC721Logic.executeBurnMultiple( _ERC721Data, POOL, + ATOMIC_PRICING, user, tokenIds ); @@ -452,7 +475,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 @@ -465,6 +512,7 @@ abstract contract MintableIncentivizedERC721 is MintableERC721Logic.executeSetIsUsedAsCollateral( _ERC721Data, POOL, + ATOMIC_PRICING, tokenId, useAsCollateral, sender @@ -487,23 +535,24 @@ 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( _ERC721Data, POOL, + ATOMIC_PRICING, tokenIds[index], useAsCollateral, sender ); } - newCollateralizedBalance = _ERC721Data - .userState[sender] - .collateralizedBalance; + newCollateralizedBalance = + _ERC721Data.userState[sender].collateralizedBalance + + _ERC721Data.userState[sender].atomicCollateralizedBalance; } /// @inheritdoc ICollateralizableERC721 @@ -526,6 +575,64 @@ 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, + ATOMIC_PRICING, + tokenId + ); + } + + /// @inheritdoc IAtomicCollateralizableERC721 + function isAtomicPricing() external view virtual returns (bool) { + return ATOMIC_PRICING; + } + + /// @inheritdoc IAtomicCollateralizableERC721 + function getTraitMultiplier(uint256 tokenId) + external + view + returns (uint256) + { + return _ERC721Data.traitsMultipliers[tokenId]; + } + + /// @inheritdoc IAtomicCollateralizableERC721 + function getTraitMultiplierSumOfAllCollateralized(address user) + external + view + returns (uint256 totalMultiplier) + { + uint256 atomicCollateralizedBalance = _ERC721Data + .userState[user] + .atomicCollateralizedBalance; + + uint256 collateralizedTokens; + for ( + uint256 index = 0; + 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 @@ -548,6 +655,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 +712,32 @@ abstract contract MintableIncentivizedERC721 is override returns (uint256) { + uint256 balance = _ERC721Data.userState[owner].balance; + uint256 atomicBalance = _ERC721Data.userState[owner].atomicBalance; + require( + index < balance + atomicBalance, + "ERC721Enumerable: owner index out of bounds" + ); + if (index < balance) { + return _ERC721Data.ownedTokens[owner][index]; + } else { + return _ERC721Data.ownedAtomicTokens[owner][index - balance]; + } + } + + function atomicTokenOfOwnerByIndex(address owner, uint256 index) + external + view + virtual + override + returns (uint256) + { + uint256 atomicBalance = _ERC721Data.userState[owner].atomicBalance; require( - index < balanceOf(owner), + index < atomicBalance, "ERC721Enumerable: owner index out of bounds" ); - return _ERC721Data.ownedTokens[owner][index]; + return _ERC721Data.ownedAtomicTokens[owner][index]; } /** diff --git a/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol b/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol index e8345fa4a..536958670 100644 --- a/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol +++ b/contracts/protocol/tokenization/libraries/MintableERC721Logic.sol @@ -3,6 +3,8 @@ 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 {Helpers} from "../../libraries/helpers/Helpers.sol"; import {IERC20} from "../../../dependencies/openzeppelin/contracts/IERC20.sol"; import "../../../interfaces/IRewardController.sol"; import "../../libraries/types/DataTypes.sol"; @@ -13,6 +15,8 @@ struct UserState { uint64 balance; uint64 collateralizedBalance; uint128 additionalData; + uint64 atomicBalance; + uint64 atomicCollateralizedBalance; } struct MintableERC721Data { @@ -42,6 +46,22 @@ 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 + mapping(uint256 => uint256) ownedAtomicTokensIndex; + // All atomic tokens' traits multipliers + mapping(uint256 => uint256) traitsMultipliers; +} + +struct LocalVars { + uint64 balance; + uint64 atomicBalance; + uint64 oldCollateralizedBalance; + uint64 oldAtomicCollateralizedBalance; + uint64 collateralizedTokens; + uint64 collateralizedAtomicTokens; } /** @@ -50,6 +70,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 = 0e18; + /** * @dev Emitted when `tokenId` token is transferred from `from` to `to`. */ @@ -80,7 +111,7 @@ library MintableERC721Logic { function executeTransfer( MintableERC721Data storage erc721Data, IPool POOL, - bool ATOMIC_PRICING, + bool atomic_pricing, address from, address to, uint256 tokenId @@ -94,18 +125,34 @@ library MintableERC721Logic { !isAuctioned(erc721Data, POOL, tokenId), Errors.TOKEN_IN_AUCTION ); - - _beforeTokenTransfer(erc721Data, from, to, tokenId); + bool isAtomic = isAtomicToken(erc721Data, atomic_pricing, 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 +165,13 @@ library MintableERC721Logic { rewardControllerLocal.handleAction( from, oldTotalSupply, - oldSenderBalance + oldSenderBalance + oldSenderAtomicBalance ); if (from != to) { rewardControllerLocal.handleAction( to, oldTotalSupply, - oldRecipientBalance + oldRecipientBalance + oldRecipientAtomicBalance ); } } @@ -135,7 +182,7 @@ library MintableERC721Logic { function executeTransferCollateralizable( MintableERC721Data storage erc721Data, IPool POOL, - bool ATOMIC_PRICING, + bool atomic_pricing, address from, address to, uint256 tokenId @@ -143,16 +190,21 @@ library MintableERC721Logic { isUsedAsCollateral_ = erc721Data.isUsedAsCollateral[tokenId]; if (from != to && isUsedAsCollateral_) { - erc721Data.userState[from].collateralizedBalance -= 1; + if (isAtomicToken(erc721Data, atomic_pricing, 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, atomic_pricing, from, to, tokenId); } function executeSetIsUsedAsCollateral( MintableERC721Data storage erc721Data, IPool POOL, + bool atomic_pricing, uint256 tokenId, bool useAsCollateral, address sender @@ -170,39 +222,48 @@ 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, atomic_pricing, 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, + 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,11 +278,13 @@ library MintableERC721Logic { tokenId, oldTotalSupply + index ); + bool isAtomic = isAtomicToken(erc721Data, atomic_pricing, tokenId); _addTokenToOwnerEnumeration( erc721Data, to, tokenId, - oldBalance + index + isAtomic ? vars.atomicBalance++ : vars.balance++, + isAtomic ); erc721Data.owners[tokenId] = to; @@ -231,51 +294,63 @@ 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; - uint64 newBalance = oldBalance + uint64(tokenData.length); - _checkBalanceLimit(erc721Data, ATOMIC_PRICING, newBalance); - erc721Data.userState[to].balance = newBalance; + _checkAtomicBalanceLimit(erc721Data, vars.atomicBalance); + + erc721Data.userState[to].balance = vars.balance; + erc721Data.userState[to].atomicBalance = vars.atomicBalance; // calculate incentives IRewardController rewardControllerLocal = erc721Data.rewardController; if (address(rewardControllerLocal) != address(0)) { - rewardControllerLocal.handleAction(to, oldTotalSupply, oldBalance); + rewardControllerLocal.handleAction( + to, + oldTotalSupply, + vars.balance + vars.atomicBalance - tokenData.length + ); } - return (oldCollateralizedBalance, newCollateralizedBalance); + return ( + vars.oldCollateralizedBalance + vars.oldAtomicCollateralizedBalance, + newCollateralizedBalance + newAtomicCollateralizedBalance + ); } function executeBurnMultiple( MintableERC721Data storage erc721Data, IPool POOL, + bool atomic_pricing, address user, uint256[] calldata tokenIds ) external returns ( - uint64 oldCollateralizedBalance, - uint64 newCollateralizedBalance + uint64 oldTotalCollateralizedBalance, + uint64 newTotalCollateralizedBalance ) { - 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 +366,18 @@ library MintableERC721Logic { tokenId, oldTotalSupply - index ); + bool isAtomic = isAtomicToken(erc721Data, atomic_pricing, tokenId); _removeTokenFromOwnerEnumeration( erc721Data, user, tokenId, - oldBalance - index + isAtomic ? vars.atomicBalance-- : vars.balance--, + isAtomic ); // Clear approvals _approve(erc721Data, address(0), tokenId); - balanceToBurn++; delete erc721Data.owners[tokenId]; if (erc721Data.auctions[tokenId].startTime > 0) { @@ -310,18 +386,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.balance; + erc721Data.userState[user].atomicBalance = vars.atomicBalance; + + 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 +416,14 @@ library MintableERC721Logic { rewardControllerLocal.handleAction( user, oldTotalSupply, - oldBalance + vars.balance + vars.atomicBalance + tokenIds.length ); } - return (oldCollateralizedBalance, newCollateralizedBalance); + return ( + vars.oldCollateralizedBalance + vars.oldAtomicCollateralizedBalance, + newCollateralizedBalance + newAtomicCollateralizedBalance + ); } function executeApprove( @@ -399,20 +488,104 @@ 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 = Helpers.isTraitMultiplierEffective( + oldMultiplier ); + bool isAtomicNext = Helpers.isTraitMultiplierEffective( + multipliers[i] + ); + + 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,30 @@ library MintableERC721Logic { return erc721Data.owners[tokenId] != address(0); } + function _cache(MintableERC721Data storage erc721Data, address user) + private + view + returns (LocalVars memory vars) + { + vars.balance = erc721Data.userState[user].balance; + vars.atomicBalance = erc721Data.userState[user].atomicBalance; + vars.oldCollateralizedBalance = erc721Data + .userState[user] + .collateralizedBalance; + vars.oldAtomicCollateralizedBalance = erc721Data + .userState[user] + .atomicCollateralizedBalance; + } + + function isAtomicToken( + MintableERC721Data storage erc721Data, + bool atomic_pricing, + uint256 tokenId + ) public view returns (bool) { + uint256 multiplier = erc721Data.traitsMultipliers[tokenId]; + return atomic_pricing || Helpers.isTraitMultiplierEffective(multiplier); + } + function isAuctioned( MintableERC721Data storage erc721Data, IPool POOL, @@ -452,26 +649,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 +688,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 +726,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..9b089012a 100644 --- a/contracts/ui/UiPoolDataProvider.sol +++ b/contracts/ui/UiPoolDataProvider.sol @@ -10,6 +10,8 @@ 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 {XTokenType, IXTokenType} from "../interfaces/IXTokenType.sol"; import {IAuctionableERC721} from "../interfaces/IAuctionableERC721.sol"; import {INToken} from "../interfaces/INToken.sol"; import {IVariableDebtToken} from "../interfaces/IVariableDebtToken.sol"; @@ -24,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; @@ -163,7 +166,6 @@ contract UiPoolDataProvider is IUiPoolDataProvider { ).name(); } - reserveData.isAtomicPricing = false; if (reserveData.underlyingAsset != SAPE_ADDRESS) { reserveData.availableLiquidity = IERC20Detailed( reserveData.underlyingAsset @@ -179,8 +181,9 @@ contract UiPoolDataProvider is IUiPoolDataProvider { reserveData.availableLiquidity = IERC721( reserveData.underlyingAsset ).balanceOf(reserveData.xTokenAddress); - reserveData.isAtomicPricing = INToken(reserveData.xTokenAddress) - .getAtomicPricingConfig(); + reserveData.isAtomicPricing = IAtomicCollateralizableERC721( + reserveData.xTokenAddress + ).isAtomicPricing(); } ( @@ -494,14 +497,21 @@ contract UiPoolDataProvider is IUiPoolDataProvider { tokenData.tokenId ); // token price - if (INToken(baseData.xTokenAddress).getAtomicPricingConfig()) { + if ( + IAtomicCollateralizableERC721(baseData.xTokenAddress) + .isAtomicPricing() + ) { try oracle.getTokenPrice(tokenData.asset, tokenData.tokenId) returns (uint256 price) { tokenData.tokenPrice = price; } catch {} } else { - tokenData.tokenPrice = collectionPrice; + tokenData.tokenPrice = Helpers.getTraitBoostedTokenPrice( + baseData.xTokenAddress, + collectionPrice, + tokenData.tokenId + ); } // token auction data tokenData.auctionData = pool.getAuctionData( 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 d8261070f..b675dbb0a 100644 --- a/test/_xtoken_ntoken.spec.ts +++ b/test/_xtoken_ntoken.spec.ts @@ -1,6 +1,11 @@ import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; import {expect} from "chai"; +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"; describe("NToken general", async () => { it("TC-ntoken-01: NToken is ERC721 compatible", async () => { @@ -13,4 +18,441 @@ 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); + + // 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 or added", 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); + + // remove multiplier + 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 + ); + + // 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 () => { + 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 + ); + + 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 () => { + 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); + }); + + 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); + }); + + 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); + }); + + 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 05240c44d..10d97f296 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,28 @@ 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 = BigNumber.from("0"); + if (isNFT && nftIdsToUse) { + 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); + } almostEqual( totalCollateral, totalCollateralBefore.add(depositedAmountInBaseUnits) @@ -1030,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(