redstone-flash-storage

Putting data directly into storage is the easiest to make information accessible to smart contracts. However, the convenience comes at a high price, as the storage access is the most costly operation in EVM (20k gas for 256bit word ~ $160k for 1Mb checked 30/08/2021) making it prohibitively expensive to use.

Usage no npm install needed!

<script type="module">
  import redstoneFlashStorage from 'https://cdn.skypack.dev/redstone-flash-storage';
</script>

README

⚡️ Flash storage

Putting data directly into storage is the easiest to make information accessible to smart contracts. However, the convenience comes at a high price, as the storage access is the most costly operation in EVM (20k gas for 256bit word ~ $160k for 1Mb checked 30/08/2021) making it prohibitively expensive to use.

Flash storage implements an alternative design of providing data to smart contracts. Instead of constantly persisting data on EVM storage, the information is brought on-chain only when needed (on-demand fetching). Until that moment, the data remains available in the Arweave blockchain where data providers are incentivised to keep information accurate and up to date. Data is transferred to EVM via a mechanism based on a meta-transaction pattern and the information integrity is verified on-chain through signature checking.

💡 How it works

At a top level, transferring data to an EVM environment requires packing an extra payload to a user's transaction and processing the message on-chain.

image.png

Data packing (off-chain data encoding)

  1. Relevant data needs to be fetched from the RedStone api
  2. Data is packed into a message according to the following structure

image.png

  1. The package is appended to the original transaction message, signed and submitted to the network

All of the steps are executed automatically by the ContractWrapper and transparent to the end-user

Data unpacking (on-chain data verification)

  1. The appended data package is extracted from the msg.data
  2. The data signature is verified by checking if the signer is one of the approved providers
  3. The timestamp is also verified checking if the information is not obsolete
  4. The value that matches a given symbol is extracted from the data package

This logic is executed in the on-chain environment and we optimised the execution using a low-level assembly code to reduce gas consumption to the absolute minimum

Benchmarks

We work hard to optimise the code using solidity assembly and reduce the gas costs of our contracts. Below there is a comparison of the read operation gas costs using the most popular Chainlink Reference Data, the standard version of Redstone PriceAware contract and the optimised version where provider address is inlined at the compilation time. The scripts which generated the data together with results and transactions details could be found in our repository.

Screenshot-2021-09-05-at-17-18-25.png

📦 Installation

Install redstone-flash-storage from NPM registry

# Using yarn
yarn add redstone-flash-storage

# Using NPM
npm install redstone-flash-storage

🔥 Getting started

1. Modifying your contracts

You need to apply a minium change to the source code to enable smart contract to access data. Your contract needs to extend the PriceAware contract :

import "redstone-flash-storage/lib/contracts/message-based/PriceAware.sol";

contract YourContractName is PriceAware {

After applying the mentioned change you will be able to access the data calling the local getPriceFromMsg function. You should pass the symbol of the asset converted to bytes32:

uint256 ethPrice = getPriceFromMsg(bytes32("ETH"));

You can see all available assets and symbols in our web app.

2. Updating the interface

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.

Contract object wrapping

First, you need to import the wrapper code to your project

// Typescript
import { WrapperBuilder } from "redstone-flash-storage";

// Javascript
const { WrapperBuilder } = require("redstone-flash-storage");

Then you can wrap your ethers contract pointing to the selected Redstone data provider. You can also specify the single asset that you would like to pass to your contract. It helps to decrease transactions GAS cost, because in this case only the data for the provided asset will be passed to the contract.

const yourEthersContract = new ethers.Contract(address, abi, provider);

// connecting all provider's prices (consumes more GAS)
const wrappedContract = WrapperBuilder
                          .wrapLite(yourEthersContract)
                          .usingPriceFeed("redstone");

// connecting a single price from selected provider
const wrappedContract = WrapperBuilder
                          .wrapLite(yourEthersContract)
                          .usingPriceFeed("redstone-stocks", "AAPL");

Now you can access any of the contract's methods in exactly the same way as interacting with the ethers-js code:

wrappedContract.executeYourMethod();

Provider authorization

If you're the owner of the contract, you should authorize a data provider after the contract deployment. You should do it before users will interact with your contract. Because the provider authenticity will be checked via signature verification whenever a user submits a transaction accessing the data. There are 2 ways of provider authorization:

1. Simple authorization

We recommend to use this option. It will automatically authorize the correct public address based on your configured price feed.

await wrappedContract.authorizeProvider();
2. Authorization by ethereum address

This option requires the provider's ethereum address. You can check the redstone providers' details using RedStone API.

await yourEthersContract.authorizeSigner("REPLACE_WITH_DATA_PROVIDER_ETHEREUM_ADDRESS")

Mock provider

If you'd like to use the wrapper in a test context, we recommend using a mock provider when you can easily override the price to test different scenarios:

Option 1. Object with prices
const wrappedContract = WrapperBuilder
                          .mockLite(yourEthersContract)
                          .using({'ETH': 2005, 'BTC': 45000, 'REDSTONE': 100000});
Option 2. Function (timestamp => PricePackage)
function mockPriceFun(curTimestamp) {
  return {
    timestamp: curTimestamp - 5000,
    prices: [
      { symbol: 'ETH', value: 2005 },
      { symbol: 'BTC', value: 45000 },
    ]
  }
}

const wrappedContract = WrapperBuilder
                          .mockLite(yourEthersContract)
                          .using(mockPriceFun);

We're also working on a wrapper for the truffle/web3 contracts. Please let us know if you need a solution for other frameworks as well.

Alternative solutions

If you don't want to modify even a single line of your contract, it's possible to use an alternative solution based on the Proxy pattern. This approach intercepts a transaction at a proxy stage, extracts the price data and delegates the original transaction to your contract. Another advantage of the solution is allowing any contract (including 3rd party ones) to access the data. However, these benefits come at the cost of higher gas consumption. If you're interested in using this approach take a look at the contracts located in the storage-based folder and reach out to us if you need help setting up your environment.

✅ Working demo

You can see examples of redstone-flash-storage usage in our dedicated repo with examples.

👨‍💻 Development and contributions

The codebase consists of a wrapper written in typescript which is responsible for packing the data and solidity smart contracts that extract the information. We encourage anyone to build and test the code and we welcome any issues with suggestions and pull requests.

Installing the dependencies

yarn install 

[Optional] Set up secrets file

If you want to run the scripts located in the ./scripts folder from your ethereum wallet you should create a .secret.json file based on the sample.secret.json and update it with your private key. The .secret.json file should not be commited when you push your changes to github (it's added to .gitignore).

Compiling and running the tests

yarn test