💾
Data Asserter
Using the Optimistic Oracle V3 to assert arbitrary off-chain data on-chain.
This section covers the Data Asserter contract, which is available in the Optimistic Oracle V3 quick-start repo. This tutorial shows an example of how to build an arbitrary data asserter contract, using the UMA Optimistic Oracle V3 (OOV3) contract. This enables you to assert to the correctness of some off-chain process and store the output on-chain. This tutorial acts as a starting point to show how the OOV3 can be flexibility used in a sample integration.
This smart contract allows data providers to assert the correctness of a data point, within any arbitrary data set, and bring the result on-chain. This contract is designed to be maximally open ended, enabling any kind of data to be asserted. For example, and for illustrative purposes of this tutorial, we will assert weather data on-chain. Note, though, that this kind of assertion flow and the associated OOV3 integration could build a wide range of assertions with arbitrary complexity. For example you could use the same structure to bring on-chain complex data associated with Beacon chain validator set to build a Liquid staking derivative, enable complex off-chain computation with on-chain outputs or anything that has a defined deterministic input output relationship.
Anyone can assert a data point against the Data Asserter contract, with a known dataId and datapoint. Independent 3rd parties can then verify the correctness of this data against the dataId.
For our example we will be asserting the output of a very simple numerical computation:
69+420
. This is meant to show the basic idea without overcomplicating the off-chain part. Note, though, that the dataId
prop used here could represent any kind of unique data identifier (a row in a database table, a validator address or a complex structure used to associate any kind of data to a given value.This project uses forge as the Ethereum testing framework. You will also need to install Foundry, refer to Foundry installation documentation if you don’t have it already.
You will also need
git
for cloning the repository, as well as bash
shell and jq
tool in order to parse transaction outputs when interacting with deployed contracts.git clone https://github.com/UMAprotocol/dev-quickstart-oov3.git
cd dev-quickstart-oov3
forge install
The contract discussed in this tutorial can be found at
dev-quickstart-oov3/src/DataAsserter.sol
(here) within the repo.To initialize the state variables of the contract, the constructor takes two parameters:
- 1.
_defaultCurrency
parameter in the constructor identifies the token used for bonds of data assertions. This token should be approved as whitelisted UMA collateral. Please check Approved Collateral Types for production networks or callgetWhitelist()
on the Address Whitelist contract for any of the test networks. Note that the deployed Optimistic Oracle V3 instance already has itsdefaultCurrency
added to the whitelist, so it can also be used by the Data Asserter contract. Alternatively, you can approve a new token address withaddToWhitelist
method in the Address Whitelist contract if working in a sandboxed UMA environment. - 2.
_optimisticOracleV3
is used to locate the address of UMA Optimistic Oracle V3. Address ofOptimisticOracleV3
contact can be fetched from the relevant networks file, if you are on a live network, or you can provide your own contract instance if deploying UMA Oracle contracts in your own sandboxed testing environment.
constructor(address _defaultCurrency, address _optimisticOracleV3) {
defaultCurrency = IERC20(_defaultCurrency);
oo = OptimisticOracleV3Interface(_optimisticOracleV3);
defaultIdentifier = oo.defaultIdentifier();
}
assertDataFor
method allows any data provider to assert some value of data
for the associated dataId
. The caller sets the asserter
as the address that will receive bonds at resolution of the data assertion (this could be their address as well). function assertDataFor(
bytes32 dataId,
bytes32 data,
address asserter
) public returns (bytes32 assertionId) { ... }
Internally, this function pulls the associated assertion bond (global for all assertions within the DataAssertion contract) from the caller, makes an assertion call to the Optimistic Oracle V3 and stores the assertion within the contracts internal
assertionsData
mapping. Lastly, an event is emitted.For the context of this example we will be considering the
dataId
simply as the URL that would be called to fetch the weather data from wttr.in
and the data
value as the result from this call. Both are bytes32 encoded.The call to the Optimistic Oracle V3 looks as follows:
assertionId = oo.assertTruth(
abi.encodePacked(
"Data asserted: 0x", // in the example data is type bytes32 so we add the hex prefix 0x.
ClaimData.toUtf8Bytes(data),
" for dataId: 0x",
ClaimData.toUtf8Bytes(dataId),
" and asserter: 0x",
ClaimData.toUtf8BytesAddress(asserter),
" at timestamp: ",
ClaimData.toUtf8BytesUint(block.timestamp),
" in the DataAsserter contract at 0x",
ClaimData.toUtf8BytesAddress(address(this)),
" is valid."
),
asserter,
address(this), // Callback recipient
address(0), // No sovereign security.
assertionLiveness,
defaultCurrency,
bond,
defaultIdentifier,
bytes32(0) // No domain.
);
Lets break this down step by step:
- First, the value for the
claim
filed is constructed to be a human readable string as the concatenation of a number of different data points. This is meant to be parsable by the validation network and enable both Humans and software verifiers alike to assess the correctness of the claim. Note that within the Optimistic Oracle V3 theclaim
field (this prop in this function call) is not stored on-chain; it is simply emitted and is used by off-chain actors to verify the correctness of the claim. - Next, the
asserter
field is passed in from this function call. This is who will receive the bonds back at resolution of the assertion, assuming the asserter was correct. - Next, we have the callback recipient set to
address(this)
. The OOV3 has the concept of callbacks wherein at the settlement of an assertion the asserter can choose to optionally call out from the OO into another contract. In this context, we want the OOV3 to call back into the Data Asserter contract when the assertion is settled (this will become clearer in the following section). - We have no Sovereign security enabled here (this is an advanced topic and does not need to be covered here).
- Default liveness, bond and identifier are set.
- No domain is set (this is an external concept that helps 3rd parties identify assertions).
Once data has been asserted, the act of verifying it is passed over to the Optimistic Oracle V3 and the associated network of data verifiers. Due to the presence of a bond (reward for catching invalid assertions), we can be confident that in real world someone would verify this assertion (if the bond is high enough, and the liveness is long enough we have high guarantees of this).
If the assertion passes the
assertionLiveness
without dispute then it can be settled. The data assertion contract showcases the OOV3's concept of assertionResolvedCallback
which enables the OOV3 to settle downstream contracts that are dependent on the resolution of an assertion. Once an assertion is settleable (it has: a) passed liveness and b) not been disputed) anyone can call settleAssertion
on the OOV3. This function acts to settle the assertion within the OO and call into the callback recipient, set during the assertDataFor
function call.Looking within the Data Asserter contract, we can see what happens when the
assertionResolvedCallback
is hit from the OOV3 on settlement:// OptimisticOracleV3 resolve callback.
function assertionResolvedCallback(bytes32 assertionId, bool assertedTruthfully) public {
require(msg.sender == address(oo));
// If the assertion was true, then the data assertion is resolved.
if (assertedTruthfully) {
assertionsData[assertionId].resolved = true;
DataAssertion memory dataAssertion = assertionsData[assertionId];
emit DataAssertionResolved(dataAssertion.dataId, dataAssertion.data, dataAssertion.asserter, assertionId);
// Else delete the data assertion if it was false to save gas.
} else delete assertionsData[assertionId];
}
This function effectively:
- enforce the inbound caller is the OOV3. Else, revert
- If the assertion was resolved as truthful (it would not be if it was disputed and the dispute proved to be correct) then:
- set the data assertion status to true,
- remove the data from the temporary
dataAssertion
structure
- if it was resolved as not truthful (deleted) then simply delete it from the temp structure
assertionsData
Once settled, we can now call the
getData
function with the assertionId.
This will return the data value and the accompanying resolution status (was it resolved truthfully, or not).If we want to dispute an asserted data point one must simply call the
disputeAssertion(bytes32 assertionId, address disputer)
method on the OOV3. In here, you provide the associated assertionId
(the same field that was returned when calling assertData
and the disputer
address (the address that will receive the bonds back at the resolution of the dispute).All the unit tests covering the functionality described above are available here. To execute all of them, run:
forge test --match-path *DataAsserter*
Before deploying and interacting with the contracts export the required environment variables. Note that there are a number of ways to interact with these contracts. The simplest setup is to fork Goerli testnet into a local development environment, using Anvil, and run the steps against the fork. To create an Anvil fork run the following:
anvil --fork-url https://goerli.infura.io/v3/xxx
Replacing
xxx
with your infura key. This will create a local fork of the Goerli testnet, running on 127.0.0.1:8545
. It will also expose a test mnemonic that you can use which has pre-loaded Eth within the fork.(Note: to set environment variables, as those listed below, use the
export
syntax in your shell. for example export ETH_RPC_URL=XXX
ETHERSCAN_API_KEY
: your secret API key used for contract verification on Etherscan if deploying on a public network (not required if you want to do this against a local testnet).ETH_RPC_URL
: your RPC node used to interact with the selected network. For this tutorial it will be easiest to do this against the Goerli fork by settingETH_RPC_URL=http://127.0.0.1:8545
MNEMONIC
: your passphrase used to derive private keys of deployer (index 0) and any other addresses interacting with the contracts. The simplest setup with this is to re-use the Anvil default unlocked accounts, if you are using the local testnet environment, you can have:export MNEMONIC="test test test test test test test test test test test junk"FINDER_ADDRESS
: address of the Finder contract used to locate other UMA ecosystem contracts (in order to resolve disputes you would need to use the one from a sandboxed environment). For Goerli, you can use:export FINDER_ADDRESS=0xE60dBa66B85E10E7Fd18a67a6859E241A243950e
Use
cast
command from Foundry to locate the address of Optimistic Oracle V3 and its default bonding token:export OOV3_ADDRESS=$(cast call $FINDER_ADDRESS "getImplementationAddress(bytes32)(address)" \
$(cast --format-bytes32-string "OptimisticOracleV3"))
export DEFAULT_CURRENCY_ADDRESS=$(cast call $OOV3_ADDRESS "defaultCurrency()(address)")
You should be able to see both of these values set in your env with
echo $OOV3_ADDRESS
and echo $DEFAULT_CURRENCY_ADDRESS
To deploy the Data Asserter contract, run
forge create
command.(Note if you hit an error like
Error accessing local wallet. Did you set a private key, mnemonic or keystore?
then you might be hitting a foundry related issue that should be resolved by updating your foundry installation with foundryup
.export DATA_ASSERTER=$(forge create --json src/DataAsserter.sol:DataAsserter \
--mnemonic "$MNEMONIC" \
--constructor-args $DEFAULT_CURRENCY_ADDRESS $OOV3_ADDRESS \
| jq -r .deployedTo)
Finally, we can verify the deployed (if deployed to a public network) contract with
forge verify-contract
:forge verify-contract \
--chain-id $(cast chain-id) \
--constructor-args $(cast abi-encode "constructor(address,address)" \
$DEFAULT_CURRENCY_ADDRESS $OOV3_ADDRESS) $DATA_ASSERTER DataAsserter
The following section provides instructions on how to interact with the deployed contract from the foundry
cast
tool, though one can also use it for guidance for interacting through another interface (e.g. Remix or Etherscan).Export derivation index for the asserting user that will be needed when signing transactions:
export ASSERTER_ID=1
Make sure the user address above have sufficient funding for the gas to execute the transactions.
Next, set up the bytes32 encoding for the
dataId
and data
, as discussed in the Asserting Data section. The dataId
field will be the simplest command that can be re-produced by independent verifiers. We use bc
(arbitrary-precision arithmetic language and calculator) that should be available on most operating systems:export DATA_ID=$(cast --format-bytes32-string "echo '69+420' | bc")
echo "DATA_ID=$(echo $DATA_ID)"
The
data
field will be the asserted output of the above bc
command:export DATA=$(cast --format-bytes32-string $(echo '69+420' | bc))
echo "DATA=$(echo $DATA)"
Note that at any point you can look at your env by running
printenv
within your console to see the things we've set up to this point.Next, we will call the Data Asserter contract and assert the data point and data ID that we set within our environment. Note that on the Goerli testnet the default bond for the default currency is set to 0. This is done to somewhat simplify the interactions: you don't need to mint yourself any USDC and you don't need to approve the Data Asserter to pull it from your account. Clearly, this is different to how this would be structured in reality but for demonstration purposes it keeps thing easier.
export ASSERTION_TX=$(cast send --json \
--mnemonic "$MNEMONIC" --mnemonic-index $ASSERTER_ID \
$DATA_ASSERTER \
"assertDataFor(bytes32,bytes32,address)" $DATA_ID $DATA $DATA_ASSERTER \
| jq -r .transactionHash)
What we are doing here is calling the
assertDataFor(bytes32,bytes32,address)
with the props DATA_ID
, DATA
and DATA_ASSERTER
, that we set earlier. We are taking the output of the transaction and storing the transactionHash
within the ASSERTION_TX
variable.We can fetch the associated
assertionId
from the DataAssertionResolved
event that should be the last event in the assertion transaction. The emitted event has the structure: event DataAssertionResolved(
bytes32 indexed dataId,
bytes32 data,
address indexed asserter,
bytes32 indexed assertionId
);
We can fetch indexed props from cast by using the
receipt
function in conjunction with jq
. To fetch the 3rd indexed prop (assertionId
) from the last event, we would run:export ASSERTION_ID=$(cast receipt --json $ASSERTION_TX | jq -r .logs[-1].topics[3])
Next, we can view the data asserted object using the
--abi-decode
call. This will return the props in the assertionData
mapping, relating to this structure: struct DataAssertion {
bytes32 dataId; // The dataId that was asserted.
bytes32 data; // This could be an arbitrary data type.
address asserter; // The address that made the assertion.
bool resolved; // Whether the assertion has been resolved.
}
cast --abi-decode "assertionsData(bytes32)(bytes32,bytes32,address,bool)" \
$(cast call $DATA_ASSERTER "assertionsData(bytes32)" $ASSERTION_ID)
The last prop in the output (
resolved
) should be false
at this moment.Now, let's move forward 2 hours to go pass the challenge window of the assertion:
cast rpc evm_increaseTime 7200
cast rpc evm_mine
Now, we can submit the settlement transaction to the OOV3 as we've passed liveness:
cast send --mnemonic "$MNEMONIC" $OOV3_ADDRESS "settleAssertion(bytes32)" $ASSERTION_ID
If we call the
assertionData
mapping again, we'll see that the assertion has settled. The last prop in the output (resolved
) should be true
:cast --abi-decode "assertionsData(bytes32)(bytes32,bytes32,address,bool)" \
$(cast call $DATA_ASSERTER "assertionsData(bytes32)" $ASSERTION_ID)
We can now call the
getData
method directly and store output in IS_RESOLVED
and RESOLVED_DATA
variables:read -r IS_RESOLVED RESOLVED_DATA \
< <(cast --abi-decode "getData(bytes32)(bool,bytes32)" \
$(cast call $DATA_ASSERTER "getData(bytes32)" $ASSERTION_ID) \
| awk '{s=(NR==1?s:s " ")$0}END{print s}')
IS_RESOLVED
should now be true
:echo $IS_RESOLVED
The decoded value of
RESOLVED_DATA
should be 489
as originally asserted:cast --parse-bytes32-string $RESOLVED_DATA
This tutorial has shown you how to use the Data Asserter sample tutorial, along with the Optimistic Oracle V3. This is meant to act as a starting point for further contracts to be built on. Next steps should be looking at the other OOV3 tutorials or trying to build your own integration! If you have questions please message us on Discord or create a Github issue and we'll happily help you with your integration.
Last modified 22d ago