diff --git a/abis/NFTOracle.json b/abis/NFTOracle.json index 51dfb0c..f993fbc 100644 --- a/abis/NFTOracle.json +++ b/abis/NFTOracle.json @@ -432,6 +432,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_nftContract", + "type": "address" + } + ], + "name": "isPriceStale", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "maxPriceDeviation", @@ -528,6 +547,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "nftPriceStale", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "owner", @@ -705,6 +743,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_nftContracts", + "type": "address[]" + }, + { + "internalType": "bool", + "name": "val", + "type": "bool" + } + ], + "name": "setPriceStale", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/contracts/interfaces/INFTOracle.sol b/contracts/interfaces/INFTOracle.sol index 329af3b..de2e2cf 100644 --- a/contracts/interfaces/INFTOracle.sol +++ b/contracts/interfaces/INFTOracle.sol @@ -29,4 +29,8 @@ interface INFTOracle { function getAssetMapping(address _nftContract) external view returns (address[] memory); function isAssetMapped(address originAsset, address mappedAsset) external view returns (bool); + + function setPriceStale(address[] calldata _nftContracts, bool val) external; + + function isPriceStale(address _nftContract) external view returns (bool); } diff --git a/contracts/interfaces/INFTOracleGetter.sol b/contracts/interfaces/INFTOracleGetter.sol index 29a988a..dfee820 100644 --- a/contracts/interfaces/INFTOracleGetter.sol +++ b/contracts/interfaces/INFTOracleGetter.sol @@ -10,4 +10,6 @@ interface INFTOracleGetter { @dev returns the asset price in ETH */ function getAssetPrice(address asset) external view returns (uint256); + + function isPriceStale(address asset) external view returns (bool); } diff --git a/contracts/libraries/helpers/Errors.sol b/contracts/libraries/helpers/Errors.sol index 4c9da21..cdbb093 100644 --- a/contracts/libraries/helpers/Errors.sol +++ b/contracts/libraries/helpers/Errors.sol @@ -47,6 +47,7 @@ library Errors { string public constant VL_SPECIFIED_LOAN_NOT_BORROWED_BY_USER = "317"; string public constant VL_SPECIFIED_RESERVE_NOT_BORROWED_BY_USER = "318"; string public constant VL_HEALTH_FACTOR_HIGHER_THAN_LIQUIDATION_THRESHOLD = "319"; + string public constant VL_PRICE_STALE = "320"; //lend pool errors string public constant LP_CALLER_NOT_LEND_POOL_CONFIGURATOR = "400"; // 'The caller of the function is not the lending pool configurator' diff --git a/contracts/libraries/logic/ValidationLogic.sol b/contracts/libraries/logic/ValidationLogic.sol index 2e11f6d..deb3510 100644 --- a/contracts/libraries/logic/ValidationLogic.sol +++ b/contracts/libraries/logic/ValidationLogic.sol @@ -11,6 +11,7 @@ import {Errors} from "../helpers/Errors.sol"; import {DataTypes} from "../types/DataTypes.sol"; import {IInterestRate} from "../../interfaces/IInterestRate.sol"; import {ILendPoolLoan} from "../../interfaces/ILendPoolLoan.sol"; +import {INFTOracleGetter} from "../../interfaces/INFTOracleGetter.sol"; import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; @@ -73,6 +74,7 @@ library ValidationLogic { bool stableRateBorrowingEnabled; bool nftIsActive; bool nftIsFrozen; + bool isPriceStale; address loanReserveAsset; address loanBorrower; } @@ -121,6 +123,9 @@ library ValidationLogic { require(vars.nftIsActive, Errors.VL_NO_ACTIVE_NFT); require(!vars.nftIsFrozen, Errors.VL_NFT_FROZEN); + vars.isPriceStale = INFTOracleGetter(nftOracle).isPriceStale(nftAsset); + require(!vars.isPriceStale, Errors.VL_PRICE_STALE); + (vars.currentLtv, vars.currentLiquidationThreshold, ) = nftData.configuration.getCollateralParams(); (vars.userCollateralBalance, vars.userBorrowBalance, vars.healthFactor) = GenericLogic.calculateLoanData( diff --git a/contracts/protocol/NFTOracle.sol b/contracts/protocol/NFTOracle.sol index fec1ae1..d0611e7 100644 --- a/contracts/protocol/NFTOracle.sol +++ b/contracts/protocol/NFTOracle.sol @@ -22,6 +22,7 @@ contract NFTOracle is INFTOracle, Initializable, OwnableUpgradeable, BlockContex event FeedAdminUpdated(address indexed admin); event SetAssetData(address indexed asset, uint256 price, uint256 timestamp, uint256 roundId); event SetAssetTwapPrice(address indexed asset, uint256 price, uint256 timestamp); + event SetPriceStale(address indexed asset, bool val); struct NFTPriceData { uint256 roundId; @@ -64,6 +65,7 @@ contract NFTOracle is INFTOracle, Initializable, OwnableUpgradeable, BlockContex mapping(address => address) private _mappedAssetToOriginalAsset; uint8 public decimals; uint256 public decimalPrecision; + mapping(address => bool) public nftPriceStale; // !!! For upgradable, MUST append one new variable above !!! ////////////////////////////////////////////////////////////////////////////// @@ -394,10 +396,38 @@ contract NFTOracle is INFTOracle, Initializable, OwnableUpgradeable, BlockContex } function setPause(address _nftContract, bool val) external override onlyOwner { + requireKeyExisted(_nftContract, true); + nftPaused[_nftContract] = val; } function setTwapInterval(uint256 _twapInterval) external override onlyOwner { twapInterval = _twapInterval; } + + function setPriceStale(address[] calldata _nftContracts, bool val) public override { + address sender = _msgSender(); + if (val) { + require((sender == priceFeedAdmin) || (sender == owner()), "NFTOracle: invalid caller"); + } else { + require(sender == owner(), "NFTOracle: invalid caller"); + } + + for (uint256 i = 0; i < _nftContracts.length; i++) { + requireKeyExisted(_nftContracts[i], true); + nftPriceStale[_nftContracts[i]] = val; + emit SetPriceStale(_nftContracts[i], val); + + // Set flag for mapped assets + address[] memory mappedAddresses = _originalAssetToMappedAsset[_nftContracts[i]].values(); + for (uint256 j = 0; j < mappedAddresses.length; j++) { + nftPriceStale[mappedAddresses[j]] = val; + emit SetPriceStale(mappedAddresses[j], val); + } + } + } + + function isPriceStale(address _nftContract) public view override returns (bool) { + return nftPriceStale[_nftContract]; + } } diff --git a/deployments/deployed-contracts-sepolia.json b/deployments/deployed-contracts-sepolia.json index e43da9d..6f06152 100644 --- a/deployments/deployed-contracts-sepolia.json +++ b/deployments/deployed-contracts-sepolia.json @@ -93,11 +93,10 @@ "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" }, "NFTOracleImpl": { - "address": "0x958f36955e10FFaa0aC92aA65acB30Ef6268421c" + "address": "0xA7bb6431BF998D4e3380ed73DcB226e23E37AA27" }, "NFTOracle": { - "address": "0xF143144Fb2703C8aeefD0c4D06d29F5Bb0a9C60A", - "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" + "address": "0xF143144Fb2703C8aeefD0c4D06d29F5Bb0a9C60A" }, "InterestRate": { "address": "0x3D8F428874a7fBde38a3EFBA48740bA6dD6E767f", diff --git a/helper-hardhat-config.ts b/helper-hardhat-config.ts index 1dd7c09..0453bf0 100644 --- a/helper-hardhat-config.ts +++ b/helper-hardhat-config.ts @@ -44,7 +44,7 @@ export const NETWORKS_RPC_URL: iParamsPerNetwork = { }; export const NETWORKS_DEFAULT_GAS: iParamsPerNetwork = { - [eEthereumNetwork.sepolia]: 15 * GWEI, + [eEthereumNetwork.sepolia]: 35 * GWEI, [eEthereumNetwork.goerli]: 65 * GWEI, [eEthereumNetwork.rinkeby]: 65 * GWEI, [eEthereumNetwork.main]: 15 * GWEI, diff --git a/helpers/types.ts b/helpers/types.ts index 68b8cb4..6fc2409 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -143,6 +143,7 @@ export enum ProtocolErrors { VL_INVALID_RESERVE_ADDRESS = "316", VL_SPECIFIED_LOAN_NOT_BORROWED_BY_USER = "317", VL_SPECIFIED_RESERVE_NOT_BORROWED_BY_USER = "318", + VL_PRICE_STALE = "320", //lend pool errors LP_CALLER_NOT_LEND_POOL_CONFIGURATOR = "400", // 'The caller of the function is not the lending pool configurator' diff --git a/test/borrow-negatives.spec.ts b/test/borrow-negatives.spec.ts index c627465..d7720c7 100644 --- a/test/borrow-negatives.spec.ts +++ b/test/borrow-negatives.spec.ts @@ -14,7 +14,8 @@ import { configuration as actionsConfiguration } from "./helpers/actions"; import { configuration as calculationsConfiguration } from "./helpers/utils/calculations"; import BigNumber from "bignumber.js"; import { getReservesConfigByPool } from "../helpers/configuration"; -import { BendPools, iBendPoolAssets, IReserveParams } from "../helpers/types"; +import { BendPools, iBendPoolAssets, IReserveParams, ProtocolErrors } from "../helpers/types"; +import { getEthersSignerByAddress } from "../helpers/contracts-helpers"; const { expect } = require("chai"); @@ -75,6 +76,35 @@ makeSuite("LendPool: Borrow negative test cases", (testEnv: TestEnv) => { cachedTokenId = tokenId; }); + it("User 1 tries to borrow 1 WETH but price is stale (revert expected)", async () => { + const { users, nftOracle, bayc } = testEnv; + const user2 = users[2]; + + const feedAdminAddr = await nftOracle.priceFeedAdmin(); + const feedAdminSigner = await getEthersSignerByAddress(feedAdminAddr); + await nftOracle.connect(feedAdminSigner).setPriceStale([bayc.address], true); + + expect(cachedTokenId, "previous test case is faild").to.not.be.undefined; + const tokenId = cachedTokenId.toString(); + + await borrow( + testEnv, + user2, + "WETH", + "1", + "BAYC", + tokenId, + user2.address, + "", + "revert", + ProtocolErrors.VL_PRICE_STALE + ); + + const ownerAddr = await nftOracle.priceFeedAdmin(); + const ownerSigner = await getEthersSignerByAddress(ownerAddr); + await nftOracle.connect(ownerSigner).setPriceStale([bayc.address], false); + }); + it("User 1 tries to uses NFT as collateral to borrow 100 WETH (revert expected)", async () => { const { users } = testEnv; const user2 = users[2]; diff --git a/test/nftOracle.spec.ts b/test/nftOracle.spec.ts index 971c9b6..de6d6c5 100644 --- a/test/nftOracle.spec.ts +++ b/test/nftOracle.spec.ts @@ -1,4 +1,6 @@ +import { get } from "http"; import { TestEnv, makeSuite } from "./helpers/make-suite"; +import { getEthersSignerByAddress } from "../helpers/contracts-helpers"; const { expect } = require("chai"); @@ -592,4 +594,42 @@ makeSuite("NFTOracle", (testEnv: TestEnv) => { await mockNftOracle.setAssetData(users[1].address, 410); }); }); + + makeSuite("NFTOracle: test setPriceStale", () => { + before(async () => { + const { mockNftOracle, users } = testEnv; + await mockNftOracle.setPriceFeedAdmin(users[0].address); + }); + + it("test setPriceStale revert", async () => { + const { mockNftOracle, users, usdc } = testEnv; + + await expect(mockNftOracle.connect(users[2].signer).setPriceStale([usdc.address], true)).to.be.revertedWith( + "NFTOracle: invalid caller" + ); + + await expect(mockNftOracle.connect(users[0].signer).setPriceStale([usdc.address], false)).to.be.revertedWith( + "NFTOracle: invalid caller" + ); + + await expect(mockNftOracle.connect(users[0].signer).setPriceStale([usdc.address], true)).to.be.revertedWith( + "NFTOracle: key not existed" + ); + }); + + it("test setPriceStale normal", async () => { + const { mockNftOracle, users, usdc } = testEnv; + await mockNftOracle.addAsset(usdc.address); + + await mockNftOracle.setPriceStale([usdc.address], true); + const isStale1 = await mockNftOracle.isPriceStale(usdc.address); + expect(isStale1).to.equal(true); + + const owner = await mockNftOracle.owner(); + const ownerSigner = await getEthersSignerByAddress(owner); + await mockNftOracle.connect(ownerSigner).setPriceStale([usdc.address], false); + const isStale2 = await mockNftOracle.isPriceStale(usdc.address); + expect(isStale2).to.equal(false); + }); + }); });