Unit Testing Oracle Data-Dependency¶
Let's see how we can test Oracle Data. First we'll cover the simple test case and then we'll see how we can cover more advanced examples.
Mock Oracle¶
To test the NFT we first need to setup a mock oracle. Let's create a simple scaffolding for our test. Add a new file in test/SilverNft.t.sol
:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {SilverOunce} from "../src/SilverNft.sol";
import {OracleEntrypoint} from "erc4337-oracle/src/OracleEntrypoint.sol";
import {DataDependent} from "erc4337-oracle/src/DataDependent.sol";
contract SilverOunceTest is Test {
OracleEntrypoint oracle;
SilverOunce silvernft;
Account provider;
function setUp() public {
Oracle = new OracleEntrypoint();
provider = makeAccount("provider");
silvernft = new SilverOunce(address(this), provider.addr, address(oracle));
}
}
Adding in Data-Cost¶
Let's pretend that calling the Oracle for the data of POL/USD costs 0.001 gas tokens. Add the following to the setUp phase:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {SilverOunce} from "../src/SilverNft.sol";
import {OracleEntrypoint} from "erc4337-oracle/src/OracleEntrypoint.sol";
import {DataDependent} from "erc4337-oracle/src/DataDependent.sol";
contract SilverOunceTest is Test {
OracleEntrypoint oracle;
SilverOunce silvernft;
Account provider;
function setUp() public {
oracle = new OracleEntrypoint();
provider = makeAccount("provider");
silvernft = new SilverOunce(address(this), provider.addr, address(oracle));
//set the price for getting a data point
bytes memory prefix = "\x19Oracle Signed Price Change:\n148";
bytes32 prefixedHashMessage = keccak256(
abi.encodePacked(
prefix,
abi.encodePacked(
block.chainid,
provider.addr,
oracle.nonces(provider.addr),
silvernft.MARKET_POL(), //or keccak256("CRYPTO_POL")
uint256(0.001 ether)
)
)
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
provider.key,
prefixedHashMessage
);
vm.prank(provider.addr);
oracle.setPrice(provider.addr, 0, silvernft.MARKET_POL(), uint256(0.001 ether), r, s, v);
}
}
If we run this with forge test
nothing will happen so far, because there are no test cases:
First Unit Test¶
Let's write the first test. Add this to the Test contract:
function test_mintNft() public {
vm.warp(100000000);
DataDependent.DataRequirement[] memory dataSources = silvernft
.requirements(bytes4(keccak256("safeMint(address)")));
uint[2] memory prices = [uint(35 * 10 ** 18), 4 * 10 ** 17]; //$35 and $0,4
for (uint256 i = 0; i < dataSources.length; i++) {
assertEq(dataSources[i].provider, provider.addr); //make sure the provider is correct
uint256 value = block.timestamp * 1000 * 2 ** (8 * 26); //timestamp first, we do some bitshifting
value += 18 * 2 ** (8 * 25);
value += prices[i];
bytes memory prefix = "\x19Oracle Signed Data Op:\n168";
bytes32 prefixedHashMessage = keccak256(
abi.encodePacked(
prefix,
abi.encodePacked(
block.chainid,
provider.addr,
oracle.nonces(provider.addr),
dataSources[i].requester,
dataSources[i].dataKey,
bytes32(value)
)
)
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(
provider.key,
prefixedHashMessage
);
vm.prank(provider.addr);
oracle.storeData(
provider.addr,
dataSources[i].requester,
oracle.nonces(provider.addr),
dataSources[i].dataKey,
bytes32(value),
r,
s,
v
);
}
address alice = makeAddr("alice");
vm.deal(alice, 100000 ether);
vm.startPrank(alice);
silvernft.safeMint{value: (87.5 ether + 0.001 ether)}(alice); //$35 XAG / 0,4 POL + 0.001 Data Price
assertEq(silvernft.balanceOf(alice), 1);
}
if you run the test now it will pass:
Reverse Testing the Unit Test¶
Let's make a small change and send not enough funds so that we also test that the data-dependency works:
...
address alice = makeAddr("alice");
vm.deal(alice, 100000 ether);
vm.startPrank(alice);
silvernft.safeMint{value: (1 ether + 0.001 ether)}(alice); //$35 XAG / 0,4 POL + 0.001 Data Price
assertEq(silvernft.balanceOf(alice), 1);
...
If we run the test now, it should fail.
But it passes. Let's examine the test case further by starting it verbose logging forge test -vvvv
Let's fix our Smart Contract. There is a typo in our requirements function:
This is the corrected requirement selector for the SilverOunce.sol NFT:
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);
}
If we run the test now, we see it fails, as expected:
If we change the test to supply enough value, we get it to pass again:
...
address alice = makeAddr("alice");
vm.deal(alice, 100000 ether);
vm.startPrank(alice);
silvernft.safeMint{value: (87.5 ether + 0.001 ether)}(alice); //$35 XAG / 0,4 POL + 0.001 Data Price
assertEq(silvernft.balanceOf(alice), 1);
...
The last part is to commit everything to git:
git add . && git commit -a -m "Added test case for Silver NFT"
Now that we have a test, lets see how we can deploy this NFT to Polygon Mainnet.