Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions abis/NFTOracle.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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": [
{
Expand Down
4 changes: 4 additions & 0 deletions contracts/interfaces/INFTOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
2 changes: 2 additions & 0 deletions contracts/interfaces/INFTOracleGetter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
1 change: 1 addition & 0 deletions contracts/libraries/helpers/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
5 changes: 5 additions & 0 deletions contracts/libraries/logic/ValidationLogic.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -73,6 +74,7 @@ library ValidationLogic {
bool stableRateBorrowingEnabled;
bool nftIsActive;
bool nftIsFrozen;
bool isPriceStale;
address loanReserveAsset;
address loanBorrower;
}
Expand Down Expand Up @@ -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(
Expand Down
30 changes: 30 additions & 0 deletions contracts/protocol/NFTOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
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;
Expand Down Expand Up @@ -64,6 +65,7 @@
mapping(address => address) private _mappedAssetToOriginalAsset;
uint8 public decimals;
uint256 public decimalPrecision;
mapping(address => bool) public nftPriceStale;

// !!! For upgradable, MUST append one new variable above !!!
//////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -394,10 +396,38 @@
}

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);

Check warning on line 425 in contracts/protocol/NFTOracle.sol

View check run for this annotation

Codecov / codecov/patch

contracts/protocol/NFTOracle.sol#L424-L425

Added lines #L424 - L425 were not covered by tests
}
}
}

function isPriceStale(address _nftContract) public view override returns (bool) {
return nftPriceStale[_nftContract];
}
}
5 changes: 2 additions & 3 deletions deployments/deployed-contracts-sepolia.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,10 @@
"deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6"
},
"NFTOracleImpl": {
"address": "0x958f36955e10FFaa0aC92aA65acB30Ef6268421c"
"address": "0xA7bb6431BF998D4e3380ed73DcB226e23E37AA27"
},
"NFTOracle": {
"address": "0xF143144Fb2703C8aeefD0c4D06d29F5Bb0a9C60A",
"deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6"
"address": "0xF143144Fb2703C8aeefD0c4D06d29F5Bb0a9C60A"
},
"InterestRate": {
"address": "0x3D8F428874a7fBde38a3EFBA48740bA6dD6E767f",
Expand Down
2 changes: 1 addition & 1 deletion helper-hardhat-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const NETWORKS_RPC_URL: iParamsPerNetwork<string> = {
};

export const NETWORKS_DEFAULT_GAS: iParamsPerNetwork<number> = {
[eEthereumNetwork.sepolia]: 15 * GWEI,
[eEthereumNetwork.sepolia]: 35 * GWEI,
[eEthereumNetwork.goerli]: 65 * GWEI,
[eEthereumNetwork.rinkeby]: 65 * GWEI,
[eEthereumNetwork.main]: 15 * GWEI,
Expand Down
1 change: 1 addition & 0 deletions helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
32 changes: 31 additions & 1 deletion test/borrow-negatives.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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];
Expand Down
40 changes: 40 additions & 0 deletions test/nftOracle.spec.ts
Original file line number Diff line number Diff line change
@@ -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");

Expand Down Expand Up @@ -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);
});
});
});
Loading