Skip to content

Commit

Permalink
feat: add asset mapping (#103)
Browse files Browse the repository at this point in the history
* feat: add asset mapping

Signed-off-by: anbergo <web3data@protonmail.com>

* refactoring asset mapping

* deploy nftoracle on goerli

* fix wrong event parameter order

---------

Signed-off-by: anbergo <web3data@protonmail.com>
Co-authored-by: anbergo <web3data@protonmail.com>
  • Loading branch information
thorseldon and anbergo committed Jun 6, 2023
1 parent 6d20ec4 commit 7926b76
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 8 deletions.
4 changes: 4 additions & 0 deletions contracts/interfaces/INFTOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ interface INFTOracle {
function setPause(address _nftContract, bool val) external;

function setTwapInterval(uint256 _twapInterval) external;

function getAssetMapping(address _nftContract) external view returns (address[] memory);

function isAssetMapped(address originAsset, address mappedAsset) external view returns (bool);
}
80 changes: 76 additions & 4 deletions contracts/protocol/NFTOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@ import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Own
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {INFTOracle} from "../interfaces/INFTOracle.sol";
import {BlockContext} from "../utils/BlockContext.sol";
import {EnumerableSetUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol";

contract NFTOracle is INFTOracle, Initializable, OwnableUpgradeable, BlockContext {
using EnumerableSetUpgradeable for EnumerableSetUpgradeable.AddressSet;

modifier onlyAdmin() {
require(_msgSender() == priceFeedAdmin, "NFTOracle: !admin");
_;
}

event AssetAdded(address indexed asset);
event AssetRemoved(address indexed asset);
event AssetMappingAdded(address indexed mappedAsset, address indexed originAsset);
event AssetMappingRemoved(address indexed mappedAsset, address indexed originAsset);
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);
Expand All @@ -29,6 +34,10 @@ contract NFTOracle is INFTOracle, Initializable, OwnableUpgradeable, BlockContex
NFTPriceData[] nftPriceData;
}

//////////////////////////////////////////////////////////////////////////////
// !!! Add new variable MUST append it only, do not insert, update type & name, or change order !!!
// https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#potentially-unsafe-operations

address public priceFeedAdmin;

// key is nft contract address
Expand All @@ -46,14 +55,22 @@ contract NFTOracle is INFTOracle, Initializable, OwnableUpgradeable, BlockContex

mapping(address => bool) public nftPaused;

uint256 public twapInterval;
mapping(address => uint256) public twapPriceMap;

// Mapping from original asset to mapped asset
mapping(address => EnumerableSetUpgradeable.AddressSet) private _originalAssetToMappedAsset;
// Mapping from mapped asset to original asset
mapping(address => address) private _mappedAssetToOriginalAsset;

// !!! For upgradable, MUST append one new variable above !!!
//////////////////////////////////////////////////////////////////////////////

modifier whenNotPaused(address _nftContract) {
_whenNotPaused(_nftContract);
_;
}

uint256 public twapInterval;
mapping(address => uint256) public twapPriceMap;

function _whenNotPaused(address _nftContract) internal view {
bool _paused = nftPaused[_nftContract];
require(!_paused, "NFTOracle: nft price feed paused");
Expand Down Expand Up @@ -100,6 +117,10 @@ contract NFTOracle is INFTOracle, Initializable, OwnableUpgradeable, BlockContex

function removeAsset(address _nftContract) external onlyOwner {
requireKeyExisted(_nftContract, true);
// make sure the asset mapping is empty before remove asset
require(_originalAssetToMappedAsset[_nftContract].length() == 0, "NFTOracle: origin asset need unmapped first");
require(_mappedAssetToOriginalAsset[_nftContract] == address(0), "NFTOracle: mapped asset need unmapped first");

delete nftPriceFeedMap[_nftContract];

uint256 length = nftPriceFeedKeys.length;
Expand All @@ -113,6 +134,39 @@ contract NFTOracle is INFTOracle, Initializable, OwnableUpgradeable, BlockContex
emit AssetRemoved(_nftContract);
}

function setAssetMapping(
address originAsset,
address mappedAsset,
bool added
) public onlyOwner {
requireKeyExisted(originAsset, true);
requireKeyExisted(mappedAsset, true);

if (added) {
// extra check for mapped asset
require(_mappedAssetToOriginalAsset[mappedAsset] == address(0), "NFTOracle: mapped asset can not mapped again");
require(
_originalAssetToMappedAsset[mappedAsset].length() == (0),
"NFTOracle: mapped asset already used as original asset"
);
// extra check for origin asset
require(
_mappedAssetToOriginalAsset[originAsset] == address(0),
"NFTOracle: original asset already used as mapped asset"
);

_originalAssetToMappedAsset[originAsset].add(mappedAsset);
_mappedAssetToOriginalAsset[mappedAsset] = originAsset;

emit AssetMappingAdded(mappedAsset, originAsset);
} else {
_originalAssetToMappedAsset[originAsset].remove(mappedAsset);
_mappedAssetToOriginalAsset[mappedAsset] = address(0);

emit AssetMappingRemoved(mappedAsset, originAsset);
}
}

function setAssetData(address _nftContract, uint256 _price) external override onlyAdmin whenNotPaused(_nftContract) {
uint256 _timestamp = _blockTimestamp();
_setAssetData(_nftContract, _price, _timestamp);
Expand Down Expand Up @@ -152,9 +206,27 @@ contract NFTOracle is INFTOracle, Initializable, OwnableUpgradeable, BlockContex

emit SetAssetData(_nftContract, _price, _timestamp, len);
emit SetAssetTwapPrice(_nftContract, twapPrice, _timestamp);

// Set data for mapped assets
address[] memory mappedAddresses = _originalAssetToMappedAsset[_nftContract].values();
for (uint256 i = 0; i < mappedAddresses.length; i++) {
nftPriceFeedMap[mappedAddresses[i]].nftPriceData.push(data);
twapPriceMap[mappedAddresses[i]] = twapPrice;

emit SetAssetData(mappedAddresses[i], _price, _timestamp, len);
emit SetAssetTwapPrice(mappedAddresses[i], twapPrice, _timestamp);
}
}

function getAssetMapping(address originAsset) public view override returns (address[] memory) {
return _originalAssetToMappedAsset[originAsset].values();
}

function isAssetMapped(address originAsset, address mappedAsset) public view override returns (bool) {
return _originalAssetToMappedAsset[originAsset].contains(mappedAsset);
}

function getAssetPrice(address _nftContract) external view override returns (uint256) {
function getAssetPrice(address _nftContract) public view override returns (uint256) {
require(isExistedKey(_nftContract), "NFTOracle: key not existed");
uint256 len = getPriceFeedLength(_nftContract);
require(len > 0, "NFTOracle: no price data");
Expand Down
5 changes: 2 additions & 3 deletions deployments/deployed-contracts-goerli.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,10 @@
"deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6"
},
"NFTOracleImpl": {
"address": "0xf8c43C594826A778026c0e32ca6c3A300E122cb3"
"address": "0xf0204dC0691615819f7974606A95A4804332199C"
},
"NFTOracle": {
"address": "0xE7E268cC1D025906fe8f6b076ecc40FF1a8dfA61",
"deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6"
"address": "0xE7E268cC1D025906fe8f6b076ecc40FF1a8dfA61"
},
"InterestRate": {
"address": "0xBcca1fCa8eE76e72Ff48A79E6F146936d81ada6E",
Expand Down
118 changes: 117 additions & 1 deletion test/nftOracle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,43 @@ const { expect } = require("chai");
makeSuite("NFTOracle", (testEnv: TestEnv) => {
before(async () => {});

it("NFTOracle: Set Admin", async () => {
it("NFTOracle: Check owner (revert expected)", async () => {
const { mockNftOracle, users } = testEnv;

const mockNftOracleNotOwner = mockNftOracle.connect(users[5].signer);

await expect(mockNftOracleNotOwner.setPriceFeedAdmin(users[0].address)).to.be.revertedWith(
"Ownable: caller is not the owner"
);

await expect(mockNftOracleNotOwner.setAssets([users[0].address])).to.be.revertedWith(
"Ownable: caller is not the owner"
);
await expect(mockNftOracleNotOwner.addAsset(users[0].address)).to.be.revertedWith(
"Ownable: caller is not the owner"
);
await expect(mockNftOracleNotOwner.removeAsset(users[0].address)).to.be.revertedWith(
"Ownable: caller is not the owner"
);

await expect(mockNftOracleNotOwner.setAssetMapping(users[0].address, users[1].address, true)).to.be.revertedWith(
"Ownable: caller is not the owner"
);
});

it("NFTOracle: Check feed admin (revert expected)", async () => {
const { mockNftOracle, users } = testEnv;

const mockNftOracleNotFeedAdmin = mockNftOracle.connect(users[5].signer);

await expect(mockNftOracleNotFeedAdmin.setAssetData(users[0].address, 400)).to.be.revertedWith("NFTOracle: !admin");

await expect(mockNftOracleNotFeedAdmin.setMultipleAssetsData([users[0].address], [400])).to.be.revertedWith(
"NFTOracle: !admin"
);
});

it("NFTOracle: Set Feed Admin", async () => {
const { mockNftOracle, users } = testEnv;
const admin = await mockNftOracle.priceFeedAdmin();
await mockNftOracle.setPriceFeedAdmin(users[0].address);
Expand Down Expand Up @@ -139,6 +175,86 @@ makeSuite("NFTOracle", (testEnv: TestEnv) => {
await mockNftOracle.removeAsset(users[1].address);
});

it("NFTOracle: Set asset mapping", async () => {
const { mockNftOracle, users } = testEnv;

// add assets
await mockNftOracle.addAsset(users[0].address);
await mockNftOracle.addAsset(users[1].address);
await mockNftOracle.addAsset(users[2].address);
await mockNftOracle.addAsset(users[3].address);

// add mapping
const mappedAddresses = await mockNftOracle.getAssetMapping(users[0].address);
expect(mappedAddresses.length).to.be.equal(0);

await mockNftOracle.setAssetMapping(users[0].address, users[1].address, true);
const mappedAddresses101 = await mockNftOracle.getAssetMapping(users[0].address);
expect(mappedAddresses101.length).to.be.equal(1);
const isMapped101 = await mockNftOracle.isAssetMapped(users[0].address, users[1].address);
expect(isMapped101).to.be.equal(true);

await mockNftOracle.setAssetMapping(users[0].address, users[2].address, true);
const mappedAddresses102 = await mockNftOracle.getAssetMapping(users[0].address);
expect(mappedAddresses102.length).to.be.equal(2);
const isMapped102 = await mockNftOracle.isAssetMapped(users[0].address, users[2].address);
expect(isMapped102).to.be.equal(true);

// mapped asset can not be mapped again
await expect(mockNftOracle.setAssetMapping(users[0].address, users[1].address, true)).to.be.revertedWith(
"NFTOracle: mapped asset can not mapped again"
);
await expect(mockNftOracle.setAssetMapping(users[3].address, users[0].address, true)).to.be.revertedWith(
"NFTOracle: mapped asset already used as original asset"
);
await expect(mockNftOracle.setAssetMapping(users[1].address, users[3].address, true)).to.be.revertedWith(
"NFTOracle: original asset already used as mapped asset"
);

// update price
const currentTime = await mockNftOracle.mock_getCurrentTimestamp();
await mockNftOracle.mock_setBlockTimestamp(currentTime.add(15));
let assets: string[] = [users[0].address];
let prices: string[] = ["400"];

await mockNftOracle.setMultipleAssetsData(assets, prices);
const ogAssetPrice = await mockNftOracle.getAssetPrice(users[0].address);
expect(ogAssetPrice).to.equal("400");

const mapAsset1Price = await mockNftOracle.getAssetPrice(users[1].address);
expect(mapAsset1Price).to.equal("400");

const mapAsset2Price = await mockNftOracle.getAssetPrice(users[2].address);
expect(mapAsset2Price).to.equal("400");

// assets can not be removed before removing mapping
await expect(mockNftOracle.removeAsset(users[0].address)).to.be.revertedWith(
"NFTOracle: origin asset need unmapped first"
);
await expect(mockNftOracle.removeAsset(users[1].address)).to.be.revertedWith(
"NFTOracle: mapped asset need unmapped first"
);

// remove mapping
await mockNftOracle.setAssetMapping(users[0].address, users[1].address, false);
const mappedAddresses201 = await mockNftOracle.getAssetMapping(users[0].address);
expect(mappedAddresses201.length).to.be.equal(1);
const isMapped201 = await mockNftOracle.isAssetMapped(users[0].address, users[1].address);
expect(isMapped201).to.be.equal(false);

await mockNftOracle.setAssetMapping(users[0].address, users[2].address, false);
const mappedAddresses202 = await mockNftOracle.getAssetMapping(users[0].address);
expect(mappedAddresses202.length).to.be.equal(0);
const isMapped202 = await mockNftOracle.isAssetMapped(users[0].address, users[2].address);
expect(isMapped202).to.be.equal(false);

// remove assets
await mockNftOracle.removeAsset(users[0].address);
await mockNftOracle.removeAsset(users[1].address);
await mockNftOracle.removeAsset(users[2].address);
await mockNftOracle.removeAsset(users[3].address);
});

it("NFTOracle: GetAssetPrice After Remove The Asset", async () => {
const { mockNftOracle, users } = testEnv;
await mockNftOracle.addAsset(users[0].address);
Expand Down

0 comments on commit 7926b76

Please sign in to comment.