Skip to content

Commit a76bb9e

Browse files
authored
feat: disable borrow when nft price is stale (#130)
* disable borrow when nft price is stale * add testcases for price stale * set flag for mapped assets
1 parent fa48839 commit a76bb9e

11 files changed

+173
-5
lines changed

abis/NFTOracle.json

+56
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,25 @@
432432
"stateMutability": "view",
433433
"type": "function"
434434
},
435+
{
436+
"inputs": [
437+
{
438+
"internalType": "address",
439+
"name": "_nftContract",
440+
"type": "address"
441+
}
442+
],
443+
"name": "isPriceStale",
444+
"outputs": [
445+
{
446+
"internalType": "bool",
447+
"name": "",
448+
"type": "bool"
449+
}
450+
],
451+
"stateMutability": "view",
452+
"type": "function"
453+
},
435454
{
436455
"inputs": [],
437456
"name": "maxPriceDeviation",
@@ -528,6 +547,25 @@
528547
"stateMutability": "view",
529548
"type": "function"
530549
},
550+
{
551+
"inputs": [
552+
{
553+
"internalType": "address",
554+
"name": "",
555+
"type": "address"
556+
}
557+
],
558+
"name": "nftPriceStale",
559+
"outputs": [
560+
{
561+
"internalType": "bool",
562+
"name": "",
563+
"type": "bool"
564+
}
565+
],
566+
"stateMutability": "view",
567+
"type": "function"
568+
},
531569
{
532570
"inputs": [],
533571
"name": "owner",
@@ -705,6 +743,24 @@
705743
"stateMutability": "nonpayable",
706744
"type": "function"
707745
},
746+
{
747+
"inputs": [
748+
{
749+
"internalType": "address[]",
750+
"name": "_nftContracts",
751+
"type": "address[]"
752+
},
753+
{
754+
"internalType": "bool",
755+
"name": "val",
756+
"type": "bool"
757+
}
758+
],
759+
"name": "setPriceStale",
760+
"outputs": [],
761+
"stateMutability": "nonpayable",
762+
"type": "function"
763+
},
708764
{
709765
"inputs": [
710766
{

contracts/interfaces/INFTOracle.sol

+4
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,8 @@ interface INFTOracle {
2929
function getAssetMapping(address _nftContract) external view returns (address[] memory);
3030

3131
function isAssetMapped(address originAsset, address mappedAsset) external view returns (bool);
32+
33+
function setPriceStale(address[] calldata _nftContracts, bool val) external;
34+
35+
function isPriceStale(address _nftContract) external view returns (bool);
3236
}

contracts/interfaces/INFTOracleGetter.sol

+2
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ interface INFTOracleGetter {
1010
@dev returns the asset price in ETH
1111
*/
1212
function getAssetPrice(address asset) external view returns (uint256);
13+
14+
function isPriceStale(address asset) external view returns (bool);
1315
}

contracts/libraries/helpers/Errors.sol

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ library Errors {
4949
string public constant VL_SPECIFIED_LOAN_NOT_BORROWED_BY_USER = "317";
5050
string public constant VL_SPECIFIED_RESERVE_NOT_BORROWED_BY_USER = "318";
5151
string public constant VL_HEALTH_FACTOR_HIGHER_THAN_LIQUIDATION_THRESHOLD = "319";
52+
string public constant VL_PRICE_STALE = "320";
5253

5354
//lend pool errors
5455
string public constant LP_CALLER_NOT_LEND_POOL_CONFIGURATOR = "400"; // 'The caller of the function is not the lending pool configurator'

contracts/libraries/logic/ValidationLogic.sol

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {Errors} from "../helpers/Errors.sol";
1111
import {DataTypes} from "../types/DataTypes.sol";
1212
import {IInterestRate} from "../../interfaces/IInterestRate.sol";
1313
import {ILendPoolLoan} from "../../interfaces/ILendPoolLoan.sol";
14+
import {INFTOracleGetter} from "../../interfaces/INFTOracleGetter.sol";
1415

1516
import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
1617
import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
@@ -73,6 +74,7 @@ library ValidationLogic {
7374
bool stableRateBorrowingEnabled;
7475
bool nftIsActive;
7576
bool nftIsFrozen;
77+
bool isPriceStale;
7678
address loanReserveAsset;
7779
address loanBorrower;
7880
}
@@ -121,6 +123,9 @@ library ValidationLogic {
121123
require(vars.nftIsActive, Errors.VL_NO_ACTIVE_NFT);
122124
require(!vars.nftIsFrozen, Errors.VL_NFT_FROZEN);
123125

126+
vars.isPriceStale = INFTOracleGetter(nftOracle).isPriceStale(nftAsset);
127+
require(!vars.isPriceStale, Errors.VL_PRICE_STALE);
128+
124129
(vars.currentLtv, vars.currentLiquidationThreshold, ) = nftData.configuration.getCollateralParams();
125130

126131
(vars.userCollateralBalance, vars.userBorrowBalance, vars.healthFactor) = GenericLogic.calculateLoanData(

contracts/protocol/NFTOracle.sol

+30
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ contract NFTOracle is INFTOracle, Initializable, OwnableUpgradeable, BlockContex
2222
event FeedAdminUpdated(address indexed admin);
2323
event SetAssetData(address indexed asset, uint256 price, uint256 timestamp, uint256 roundId);
2424
event SetAssetTwapPrice(address indexed asset, uint256 price, uint256 timestamp);
25+
event SetPriceStale(address indexed asset, bool val);
2526

2627
struct NFTPriceData {
2728
uint256 roundId;
@@ -64,6 +65,7 @@ contract NFTOracle is INFTOracle, Initializable, OwnableUpgradeable, BlockContex
6465
mapping(address => address) private _mappedAssetToOriginalAsset;
6566
uint8 public decimals;
6667
uint256 public decimalPrecision;
68+
mapping(address => bool) public nftPriceStale;
6769

6870
// !!! For upgradable, MUST append one new variable above !!!
6971
//////////////////////////////////////////////////////////////////////////////
@@ -394,10 +396,38 @@ contract NFTOracle is INFTOracle, Initializable, OwnableUpgradeable, BlockContex
394396
}
395397

396398
function setPause(address _nftContract, bool val) external override onlyOwner {
399+
requireKeyExisted(_nftContract, true);
400+
397401
nftPaused[_nftContract] = val;
398402
}
399403

400404
function setTwapInterval(uint256 _twapInterval) external override onlyOwner {
401405
twapInterval = _twapInterval;
402406
}
407+
408+
function setPriceStale(address[] calldata _nftContracts, bool val) public override {
409+
address sender = _msgSender();
410+
if (val) {
411+
require((sender == priceFeedAdmin) || (sender == owner()), "NFTOracle: invalid caller");
412+
} else {
413+
require(sender == owner(), "NFTOracle: invalid caller");
414+
}
415+
416+
for (uint256 i = 0; i < _nftContracts.length; i++) {
417+
requireKeyExisted(_nftContracts[i], true);
418+
nftPriceStale[_nftContracts[i]] = val;
419+
emit SetPriceStale(_nftContracts[i], val);
420+
421+
// Set flag for mapped assets
422+
address[] memory mappedAddresses = _originalAssetToMappedAsset[_nftContracts[i]].values();
423+
for (uint256 j = 0; j < mappedAddresses.length; j++) {
424+
nftPriceStale[mappedAddresses[j]] = val;
425+
emit SetPriceStale(mappedAddresses[j], val);
426+
}
427+
}
428+
}
429+
430+
function isPriceStale(address _nftContract) public view override returns (bool) {
431+
return nftPriceStale[_nftContract];
432+
}
403433
}

deployments/deployed-contracts-sepolia.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,10 @@
9393
"deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6"
9494
},
9595
"NFTOracleImpl": {
96-
"address": "0x958f36955e10FFaa0aC92aA65acB30Ef6268421c"
96+
"address": "0xA7bb6431BF998D4e3380ed73DcB226e23E37AA27"
9797
},
9898
"NFTOracle": {
99-
"address": "0xF143144Fb2703C8aeefD0c4D06d29F5Bb0a9C60A",
100-
"deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6"
99+
"address": "0xF143144Fb2703C8aeefD0c4D06d29F5Bb0a9C60A"
101100
},
102101
"InterestRate": {
103102
"address": "0x3D8F428874a7fBde38a3EFBA48740bA6dD6E767f",

helper-hardhat-config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const NETWORKS_RPC_URL: iParamsPerNetwork<string> = {
4444
};
4545

4646
export const NETWORKS_DEFAULT_GAS: iParamsPerNetwork<number> = {
47-
[eEthereumNetwork.sepolia]: 15 * GWEI,
47+
[eEthereumNetwork.sepolia]: 35 * GWEI,
4848
[eEthereumNetwork.goerli]: 65 * GWEI,
4949
[eEthereumNetwork.rinkeby]: 65 * GWEI,
5050
[eEthereumNetwork.main]: 15 * GWEI,

helpers/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export enum ProtocolErrors {
145145
VL_INVALID_RESERVE_ADDRESS = "316",
146146
VL_SPECIFIED_LOAN_NOT_BORROWED_BY_USER = "317",
147147
VL_SPECIFIED_RESERVE_NOT_BORROWED_BY_USER = "318",
148+
VL_PRICE_STALE = "320",
148149

149150
//lend pool errors
150151
LP_CALLER_NOT_LEND_POOL_CONFIGURATOR = "400", // 'The caller of the function is not the lending pool configurator'

test/borrow-negatives.spec.ts

+31-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import { configuration as actionsConfiguration } from "./helpers/actions";
1414
import { configuration as calculationsConfiguration } from "./helpers/utils/calculations";
1515
import BigNumber from "bignumber.js";
1616
import { getReservesConfigByPool } from "../helpers/configuration";
17-
import { BendPools, iBendPoolAssets, IReserveParams } from "../helpers/types";
17+
import { BendPools, iBendPoolAssets, IReserveParams, ProtocolErrors } from "../helpers/types";
18+
import { getEthersSignerByAddress } from "../helpers/contracts-helpers";
1819

1920
const { expect } = require("chai");
2021

@@ -75,6 +76,35 @@ makeSuite("LendPool: Borrow negative test cases", (testEnv: TestEnv) => {
7576
cachedTokenId = tokenId;
7677
});
7778

79+
it("User 1 tries to borrow 1 WETH but price is stale (revert expected)", async () => {
80+
const { users, nftOracle, bayc } = testEnv;
81+
const user2 = users[2];
82+
83+
const feedAdminAddr = await nftOracle.priceFeedAdmin();
84+
const feedAdminSigner = await getEthersSignerByAddress(feedAdminAddr);
85+
await nftOracle.connect(feedAdminSigner).setPriceStale([bayc.address], true);
86+
87+
expect(cachedTokenId, "previous test case is faild").to.not.be.undefined;
88+
const tokenId = cachedTokenId.toString();
89+
90+
await borrow(
91+
testEnv,
92+
user2,
93+
"WETH",
94+
"1",
95+
"BAYC",
96+
tokenId,
97+
user2.address,
98+
"",
99+
"revert",
100+
ProtocolErrors.VL_PRICE_STALE
101+
);
102+
103+
const ownerAddr = await nftOracle.priceFeedAdmin();
104+
const ownerSigner = await getEthersSignerByAddress(ownerAddr);
105+
await nftOracle.connect(ownerSigner).setPriceStale([bayc.address], false);
106+
});
107+
78108
it("User 1 tries to uses NFT as collateral to borrow 100 WETH (revert expected)", async () => {
79109
const { users } = testEnv;
80110
const user2 = users[2];

test/nftOracle.spec.ts

+40
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { get } from "http";
12
import { TestEnv, makeSuite } from "./helpers/make-suite";
3+
import { getEthersSignerByAddress } from "../helpers/contracts-helpers";
24

35
const { expect } = require("chai");
46

@@ -592,4 +594,42 @@ makeSuite("NFTOracle", (testEnv: TestEnv) => {
592594
await mockNftOracle.setAssetData(users[1].address, 410);
593595
});
594596
});
597+
598+
makeSuite("NFTOracle: test setPriceStale", () => {
599+
before(async () => {
600+
const { mockNftOracle, users } = testEnv;
601+
await mockNftOracle.setPriceFeedAdmin(users[0].address);
602+
});
603+
604+
it("test setPriceStale revert", async () => {
605+
const { mockNftOracle, users, usdc } = testEnv;
606+
607+
await expect(mockNftOracle.connect(users[2].signer).setPriceStale([usdc.address], true)).to.be.revertedWith(
608+
"NFTOracle: invalid caller"
609+
);
610+
611+
await expect(mockNftOracle.connect(users[0].signer).setPriceStale([usdc.address], false)).to.be.revertedWith(
612+
"NFTOracle: invalid caller"
613+
);
614+
615+
await expect(mockNftOracle.connect(users[0].signer).setPriceStale([usdc.address], true)).to.be.revertedWith(
616+
"NFTOracle: key not existed"
617+
);
618+
});
619+
620+
it("test setPriceStale normal", async () => {
621+
const { mockNftOracle, users, usdc } = testEnv;
622+
await mockNftOracle.addAsset(usdc.address);
623+
624+
await mockNftOracle.setPriceStale([usdc.address], true);
625+
const isStale1 = await mockNftOracle.isPriceStale(usdc.address);
626+
expect(isStale1).to.equal(true);
627+
628+
const owner = await mockNftOracle.owner();
629+
const ownerSigner = await getEthersSignerByAddress(owner);
630+
await mockNftOracle.connect(ownerSigner).setPriceStale([usdc.address], false);
631+
const isStale2 = await mockNftOracle.isPriceStale(usdc.address);
632+
expect(isStale2).to.equal(false);
633+
});
634+
});
595635
});

0 commit comments

Comments
 (0)