By the end of this tutorial you will understand how to integrate your dApp built on zkSync with RedStone oracles.
This repo is designed to show how to build a dApp that uses RedStone oracles on zkSync.
The repo contains an implementation of an NFT marketplace dApp with so-called "stable" price. It means that sellers can create sell orders (offers), specifying price amount in USD. But buyers are able to pay with native coins, required amount of which is calculated dynamically in the moment of the order execution. Repo lacks few crucial parts which will demonstrate how to integrate RedStone oracles and deploy dApp on zkSync Era Testnet.
We use hardhat, version prepared for working on zkSync, and ethers.js for deployment scripts and contract tests. Frontend is implemented in React.
βββ contracts # Solidity contracts
β βββ ExampleNFT.sol # Example ERC721 contract
β βββ Marketplace.sol # Simple NFT marketplace contract
β βββ StableMarketplace.sol # NFT marketplace contract with stable price
β βββ ...
βββ public # Folder with public html files and images for React app
βββ deploy # Contract deployment script
βββ src # React app source code
β βββ components
β β βββ App.tsx # Main React component
β βββ core
β β βββ blockchain.ts # TS module responsible for interaction with blockchain and contracts
β βββ config/ # Folder with contract ABIs and deployed contract addresses
β βββ ...
βββ test # Contract tests
βββ ...
ExampleNFT
is a simple ERC721 contract with automated sequential token id assignment
function mint() external {
_mint(msg.sender, nextTokenId);
nextTokenId++;
}
This contract extends ERC721Enumerable
implementation created by the @openzeppelin
team, which adds view functions for listing all tokens and tokens owned by a user.
Marketplace
is an NFT marketplace contract, which allows to post sell orders for any NFT token that follows EIP-721 non-fungible token standard. It has the following functions:
// Created a new sell order
// This function requires approval for transfer on the specified NFT token
function postSellOrder(address nftContractAddress, uint256 tokenId, uint256 price) external {}
// Only order creator can call this function
function cancelOrder(uint256 orderId) external {}
// Allows to get info about all orders (including canceled, and executed ones)
function getAllOrders() public view returns (SellOrder[] memory) {}
// Returns expected price in ETH for the given order
function getPrice(uint256 orderId) public view returns (uint256) {}
// Requires sending at least the minimal amount of ETH
function buy(uint256 orderId) external payable {}
The implementation is quite straightforward, so we won't describe it here. You can check the full contract code in the contracts/Marketplace.sol.
StableMarketplace
is the marketplace contract with the stable price support. It extends the Marketplace.sol
implementation and only overrides its _getPriceFromOrder
function.
This contract will integrate RedStone oracles functionalities and will be described later.
You can check the code of the React app in the src
folder. We tried to simplify it as much as possible and leave only the core marketplace functions.
The main UI logic is located in the App.tsx
file, and the contract interaction logic is in the blockchain.ts
file.
If you take a look into the blockchain.ts
file code, you'll notice that each contract call that needs to process RedStone data is made on a contract instance, that was wrapped by @redstone-finance/evm-connector.
We've used hardhat test framework to contract tests. All the tests are located in the test folder.
git clone https://github.com/redstone-finance/stable-price-marketplace
cd stable-price-marketplace
yarn install
You can run zkSync node in dockerized setup by following instructions presented here
If you are not familiar with the code yet, please read implementation description
Now it is time to integrate RedStone Oracles into the marketplace. As you maybe noticed some parts of the code are missing the implementation. Let me give you instructions on how to integrate RedStone oracles.Β
First, we need to modify contracts as currently, they are not ready to receive transactions with RedStone data regarding price. If you are not familiar with our core model please read how to adjust your smart contracts.Β Take a look at the StableMarketplace contract. It is the marketplace contract with stable price support. It extends the Marketplace.sol
implementation and only overrides its _getPriceFromOrder
function. The contract should be extended by MainDemoConsumerBase which is imported from @redstone-finance/evm-connector. The _getPriceFromOrder
function should use the getOracleNumericValueFromTxMsg
function to get price data and calculate the final price based on the order price and the price of ETH. Full implementation can be seen below:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@redstone-finance/evm-connector/contracts/data-services/MainDemoConsumerBase.sol";
import "./Marketplace.sol";
/*
StableMarketplace contract should extend MainDemoConsumerBase contract
For being able to use redstone oracles data, more inf:
https://docs.redstone.finance/docs/smart-contract-devs/get-started/redstone-core#1-adjust-your-smart-contracts
*/
contract StableMarketplace is Marketplace, MainDemoConsumerBase {
/*
`_getPriceFromOrder` function should uses the `getOracleNumericValueFromTxMsg` function,
which fetches signed data from tx calldata and verifies its signature
*/
function _getPriceFromOrder(
SellOrder memory order
) internal view override returns (uint256) {
uint256 ethPrice = getOracleNumericValueFromTxMsg(bytes32("ETH"));
return (order.price / ethPrice) * (10 ** 8);
}
}
The second thing to do is adjust the Typescript code of the dApp. Please take a look at the blockchain.ts
file. Here you can find all functions required to make the marketplace work. But the function buy
is not implemented. Here we will call the function from the contracts which require price data. To make it possible we need to wrap the contract instance with the RedStone framework.Β If you are not familiar with our core model please read how to adjust Typescript code. After wrapping the contract we will be able to callΒ the getPrice
function from the StableMarketplace
contract which eventually will call overridden _getPriceFromOrder
. Now we are able to call the buy
function from the StableMarketplace
contract with the expected ETH amount to buy the NFT.Β Full implementation can be seen below:
import { WrapperBuilder } from "@redstone-finance/evm-connector";
async function getContractInstance(contractName) {
...
return new ethers.Contract(address, abi, signer);
}
async function buy(orderId) {
const marketplace = await getContractInstance("marketplace");
// Wrapping marketplace contract instance.
// It enables fetching data from redstone data pool
// for each contract function call
try {
const wrappedMarketplaceContract = WrapperBuilder.wrap(
marketplace
).usingDataService(
{
dataServiceId: "redstone-main-demo",
uniqueSignersCount: 1,
dataFeeds: ["ETH"],
},
);
// Checking expected amount
const expectedEthAmount = await wrappedMarketplaceContract.getPrice(orderId);
// Sending buy tx
const buyTx = await wrappedMarketplaceContract.wi(orderId, {
value: expectedEthAmount.mul(101).div(100), // a buffer for price movements
});
await buyTx.wait();
return buyTx;
} catch {
const errText = "Error happened while buying the NFT";
alert(errText);
}
}
yarn test
yarn compile
You need to populate .env file with private key for deployment e.g.
WALLET_PRIVATE_KEY=0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110
and then run
yarn deploy:local
yarn app:start
The app should be running on http://localhost:3000
Select Networks dropdown
-> Add network
and enter the following details:
Network Name | hardhat-local |
---|---|
New RPC URL | http://localhost:3050 |
Chain ID | 270 |
Currency Symbol | ETH |
Then hit the Save
button.
User 1
:0xac1e735be8536c6534bb4f17f06f6afc73b2b5ba84ac2cfb12f7461b20c0bbe3
User 2
:0xd293c684d884d56f8d6abd64fc76757d3664904e309a0645baf8522ab6366d9e
After visiting the app first time you will see an almost empty screen with the + Mint new NFT
link. Click this link to mint new NFTs. After the minting transaction confirmation you will see your NFT in the left column.
Once you mint any NFTs, you can post sell order for each one of them. Click the SELL button and provide the USD value. You will be asked to confirm 2 transactions: for NFT transfer approval, and for the marketplace order creation. After their confirmation, you will see your order in the Orders column.
You can also switch metamask account and buy the NFT. I would recommend to open the developer tools in browser at the network tab and explore network requests that are being sent before the buy transaction sending.
You should see at least 2 requests with the ETH price data and crypto signatures. This data along with signatures is being attached for each contract call, that wants to process redstone oracles data.
RedStone is a data ecosystem that delivers frequently updated, reliable and diverse data for your dApps and smart contracts.
RedStone offers a radically different design of Oracles catering to the needs of modern DeFi protocols.
- Data providers can avoid the requirement of continuous on-chain data delivery
- Allow end users to self-deliver signed Oracle data on-chain
- Use the decentralized Streamr network to deliver signed oracle data to the end users
- Use token incentives to motivate data providers to maintain data integrity and uninterrupted service
- Leverage the Arweave blockchain as cheap and permanent storage for archiving Oracle data and maintaining data providers' accountability
To learn more about RedStone oracles design check out the RedStone docs.
- The modular architecture maintains data integrity from source to smart contracts
- There are 3 different ways to integrate our service tailored to your needs
- We provide feeds for more than 1000 assets integrating ~50 data sources
- We are present on 20+ chains
- RedStone has been live on mainnets since March 2022 with no downtime. Code was audited by ABDK, Packshield and L2Beat Co-Founder.
- RedStone was a launch partner for DeltaPrime on Avalanche and delivered data feeds not available anywhere else. Thanks to that DeltaPrime became the top 3 fastest growing dApps according to DefiLama.
Thanks to our innovative architecture, we offer more than one thousand of pricing data feeds, including tokens, stocks, ETFs, commodities, and much more for a fraction of regular Oracles integration costs.
You can check available assets and data providers using app.redstone.finance.
IMPORTANT: Please reach out to the RedStone team on Discord before using RedStone oracles in production dApps. We will be happy to help you with the integration and will set up a new pool of data provider nodes if there is a need.
Install @redstone-finance/evm-connector from NPM registry
# Using yarn
yarn add @redstone-finance/evm-connector
# Using NPM
npm install @redstone-finance/evm-connector
TLDR; You need to do 2 things:
- Adjust your smart contracts
- Adjust Javascript code of your dApp (it is required, otherwise you will get smart contract errors)
π‘ Note: Please don't use Remix to test RedStone oracles, as Remix does not support modifying transactions in the way that the evm-connector does.
You need to apply a minimum change to the source code to enable smart contract to access data. Your contract needs to extend one of our custom base contracts, which can be found here.
We strongly recommend having some upgradeability mechanism for your contracts (it can be based on multisig, DAO, or anything else). This way, you can quickly switch to the latest trusted data providers in case of changes or problems with the current providers.
import "@redstone-finance/evm-connector/contracts/data-services/MainDemoConsumerBase.sol";
contract YourContractName is MainDemoConsumerBase {
...
}
After applying the mentioned change you will be able to access the data calling the local getOracleNumericValueFromTxMsg
function. You should pass the data feed id converted to bytes32
.
// Getting a single value
uint256 ethPrice = getOracleNumericValueFromTxMsg(bytes32("ETH"));
// Getting several values
bytes32[] memory dataFeedIds = new bytes32[](2);
dataFeedIds[0] = bytes32("ETH");
dataFeedIds[1] = bytes32("BTC");
uint256[] memory values = getOracleNumericValuesFromTxMsg(dataFeedIds);
uint256 ethPrice = values[0];
uint256 btcPrice = values[1];
You can see all available data feeds in our web app.
You should also update the code responsible for submitting transactions. If you're using ethers.js, we've prepared a dedicated library to make the transition seamless.
First, you need to import the wrapper code to your project
// Typescript
import { WrapperBuilder } from "@redstone-finance/evm-connector";
// Javascript
const { WrapperBuilder } = require("@redstone-finance/evm-connector");
Then you can wrap your ethers contract pointing to the selected Redstone data service id. You should also specify a number of unique signers, data feed identifiers, and (optionally) URLs for the redstone cache nodes.
const yourEthersContract = new ethers.Contract(address, abi, provider);
// Connecting all provider's prices (consumes more GAS)
const wrappedContract = WrapperBuilder.wrap(contract).usingDataService(
{
dataServiceId: "redstone-main-demo",
uniqueSignersCount: 1,
dataFeeds: ["ETH", "BTC"],
},
);
Now you can access any of the contract's methods in exactly the same way as interacting with the ethers-js code:
wrappedContract.executeYourMethod();
If you'd like to use the wrapper in a test context, we recommend using a mock wrapper so that you can easily override the oracles values to test different scenarios. To use the mock wrapper just use the usingMockData(signedDataPackages)
function instead of the usingDataService
function. You can see examples of the mock wrapper usage here.
You can find more information in the RedStone documentation to learn how to integrate your zkSync dApp with RedStone oracles.
- Repo with examples
- RedStone Documentation
- RedStone Price Feeds
- Data from any URL
- NFT Data Feeds
- Randomness
Please feel free to contact the RedStone team on Discord if you have any questions.