Add Oracle Data Dependency
Adding in Data-Dependency¶
Now the question is, how does the Data-Dependency and Oracle Data come in?
Let's first see how that works and why we need it.
In our NFT, we want to calculate how much money is needed in POL to purchase one NFT but for the price of an Ounce of Silver. So, our safeMint function has two data dependencies:
- It needs the current price of POL in USD
- It needs the current price of an Oz of Silver in USD
Together we can calculate how much an NFT costs (XAG/POL). The Oracle sends all prices with 18 decimals, which makes it easy to handle on chain. So, if the Price of Silver is $30 and the Price of POL is $0,4, then the user needs to send along (30/0,4)*1e18 = 75_000_000_000_000_000_000 POL along to the safeMint
function.
In our Oracle Solution we heavily rely on the Candide Account Abstraction NPM Library. We have a modified version of it, that can read what functions from the contract need which data. That is where the Data-Dependency comes in. The Morpher dd-abstractionkit is fully backwards compatible to the Candide Abstractionkit. The big difference is, it understands when a UserOp also needs a data request to the Oracle and sends it then to the right bundler endpoint.
That works through a so called DataDependent requirements
interface, where each function selector returns which data is required.
Let's add the right library to our Project:
forge install Morpher-io/erc4337-oracle
You should end up with a new folder in your /lib
folder:
Add the Data-Dependcy into the Silver NFT Contract:
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.20;
import "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol";
import "openzeppelin-contracts/contracts/access/Ownable.sol";
import "erc4337-oracle/src/DataDependent.sol";
contract SilverOunce is ERC721, Ownable, DataDependent {
uint256 private _nextTokenId;
address dataProvider; //0x110016975ce40F7cB1Dae96a9da51ae1037db3c4 on polygon
address oracleEntrypoint; //0xfc13Eca5251CDbC1ED703da32c8E3038Da227E24
bytes32 constant public MARKET_XAG = keccak256("MORPHER:COMMODITY_XAG");
bytes32 constant public MARKET_POL = keccak256("MORPHER:COMMODITY_POL");
constructor(address initialOwner, address _dataProvider, address _oracleEntrypoint)
ERC721("SilverOunce", "FXAG")
Ownable(initialOwner)
{
dataProvider = _dataProvider;
oracleEntrypoint = _oracleEntrypoint;
}
function safeMint(address to) public onlyOwner {
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
}
function requirements(
bytes4 _selector
) external override view returns (DataRequirement[] memory) {
if (_selector == bytes4(keccak256("safeMint(address"))) {
DataRequirement[] memory requirement = new DataRequirement[](2);
requirement[0] = DataRequirement(dataProvider, address(this), MARKET_XAG);
requirement[1] = DataRequirement(dataProvider, address(this), MARKET_POL);
return requirement;
}
return new DataRequirement[](0);
}
}
Consume Data From The Oracle¶
To consume data, we have to call the function _invokeOracle
from the DataDependent Contract we inherit from. Have a look at the following safeMint
function that does that:
function safeMint(address to) public payable {
//needs to be at least XAU/POL + fees
ResponseWithExpenses memory response_xag = _invokeOracle(oracleEntrypoint, dataProvider, MARKET_XAG);
ResponseWithExpenses memory response_pol = _invokeOracle(oracleEntrypoint, dataProvider, MARKET_POL);
require(msg.value >= ((response_xag.expenses + response_pol.expenses) + ((response_xag.value*1e18) / response_pol.value)), "SilverOunce: Not enough value");
//refund the remainder
uint remainder = uint(msg.value - ((response_xag.expenses + response_pol.expenses) + ((response_xag.value*1e18) / response_pol.value)));
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
//store the token price for later opensea traits
tokenPriceInUsd[tokenId] = response_xag.value;
//checks effects interactions pattern: transfer always comes last
payable(msg.sender).transfer(remainder);
//just a demo - refund everything
payable(msg.sender).transfer(address(this).balance); //just transfer everything, we dont care
}
Now the function is not only payable, but it also makes sure that the user can only mint an NFT if he sends enough msg.value
along to cover the price of an Ounce of Silver in POL.
We have one addition here to a regular function, its in tokenPriceInUsd
because I wanted to store the Silver Price a user paid at the time of purchase and show it as an NFT trait. You can skip this part, of course, depending on your use case. The important part is
ResponseWithExpenses memory response_xag = _invokeOracle(oracleEntrypoint, dataProvider, MARKET_XAG);
ResponseWithExpenses memory response_pol = _invokeOracle(oracleEntrypoint, dataProvider, MARKET_POL);
which is calling the oracle. It will return a ResponseWithExpenses
and from the DataDependent Contract you can see what it returns:
struct ResponseWithExpenses {
uint value;
uint expenses;
}
it contains the value, in this case a price fixed point 18 decimals. And the Oracle fee. Every data point might incur a small fee for the Oracle provider which needs to be paid, otherwise the UserOp won't go through.
The DataDependent call to _invokeOracle
does pay the fee to the OracleEntryPoint, so you don't have to take care of this. This happens automatically by this line:
bytes32 response = IOracleEntrypoint(oracle).consumeData{value: expenses}(_provider, _key);
It's an NFT, what about that TokenURI and JSON? After all, we might want to see it on OpenSea?
That's rather easy and since its not related to the Oracle itself, I will skip a detailed explanation. Suffice to say opensea can handle inline json:
function tokenURI(uint256 tokenId) public view override returns (string memory) {
_requireOwned(tokenId);
return string.concat(
"data:application/json;utf8,",
'{"name":"Morpher Oracle SilverToken", "description":"Demo to mind one Ounce of Silver at current Prices", "attributes":[{"trait_type": "Purchase Price","value": ',Strings.toString(tokenPriceInUsd[tokenId]),'}], "image":"data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjAwIDIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCBtZWV0Ij4KICA8cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZTBlMGUwIiAvPgogIDxjaXJjbGUgY3g9IjEwMCIgY3k9IjEwMCIgcj0iOTAiIGZpbGw9InNpbHZlciIgc3Ryb2tlPSIjYjBiMGIwIiBzdHJva2Utd2lkdGg9IjEwIiAvPgogIDxjaXJjbGUgY3g9IjEwMCIgY3k9IjEwMCIgcj0iNjAiIGZpbGw9Im5vbmUiIHN0cm9rZT0icmdiYSgyNTUsIDI1NSwgMjU1LCAwLjYpIiBzdHJva2Utd2lkdGg9IjUiIC8+CiAgPHRleHQgeD0iNTAlIiB5PSI1MCUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiNiMGIwYjAiIGZvbnQtc2l6ZT0iNTAiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZHk9Ii4zNWVtIj5BZzwvdGV4dD4KPC9zdmc+Cg==", "external_url":"https://oracle.morpher.com"}');
}
Now we would be finished, but there are more topics to cover before we can go to the NextJS app:
- How to Unit Test Oracle Data-Dependent calls?
- How do we deploy this using Foundry?
Let's check in our changes to Git first and then write a unit test.
git commit -a -m "Finalized Solidity Contract"