Library for Ink! smart contract to help you build Phat Rollup Anchor deployed on the Substrate pallet Contracts.
This library uses the OpenBrush library with the features ownable
and access_control
It provides the following features for:
KvStore
: key-value store that allows offchain Phat Contracts to perform read/write operations.MessageQueue
: Message Queue, enabling a request-response programming model for the smart-contract while ensuring that each request received exactly one response. It uses the KV Store to save the messages.RollupAnchor
: Use the kv-store and the message queue to allow offchain's rollup transactions.MetaTransaction
: Allow the offchain Phat Contract to do transactions without paying the gas fee. The fee will be paid by a third party (the relayer).
To build the crate:
cargo build
To run the integration tests:
cargo test
The default toml of your project
[dependencies]
ink = { version = "4.2.0", default-features = false }
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"], optional = true }
# OpenBrush dependency
openbrush = { git = "https://github.com/727-Ventures/openbrush-contracts", version = "4.0.0-beta", features = ["ownable", "access_control"], default-features = false }
# Phat Rollup Anchor dependency
phat_rollup_anchor_ink = { path = "phat-rollup-anchor-ink", default-features = false}
[features]
default = ["std"]
std = [
"ink/std",
"scale/std",
"scale-info/std",
"openbrush/std",
"phat_rollup_anchor_ink/std",
]
Use openbrush::contract
macro instead of ink::contract
.
Import everything from openbrush::contracts::access_control
, openbrush::contracts::ownable
, phat_rollup_anchor_ink::traits::meta_transaction
, phat_rollup_anchor_ink::traits::rollup_anchor
.
#![cfg_attr(not(feature = "std"), no_std, no_main)]
#[openbrush::implementation(Ownable, AccessControl)]
#[openbrush::contract]
pub mod test_oracle {
use openbrush::contracts::access_control::*;
use openbrush::contracts::ownable::*;
use openbrush::traits::Storage;
use scale::{Decode, Encode};
use phat_rollup_anchor_ink::traits::{
meta_transaction, meta_transaction::*,
rollup_anchor, rollup_anchor::*
};
...
Declare storage struct and declare the fields related to the modules.
#[ink(storage)]
#[derive(Default, Storage)]
pub struct TestOracle {
#[storage_field]
ownable: ownable::Data,
#[storage_field]
access: access_control::Data,
#[storage_field]
rollup_anchor: rollup_anchor::Data,
#[storage_field]
meta_transaction: meta_transaction::Data,
...
}
Inherit implementation of the traits. You can customize (override) methods in this impl
block.
impl RollupAnchor for TestOracle {}
impl MetaTransaction for TestOracle {}
impl TestOracle {
#[ink(constructor)]
pub fn new() -> Self {
let mut instance = Self::default();
let caller = instance.env().caller();
// set the owner of this contract
ownable::Internal::_init_with_owner(&mut instance, caller);
// set the admin of this contract
access_control::Internal::_init_with_admin(&mut instance, Some(caller));
instance
}
}
Implement the rollup_anchor::EventBroadcaster
trait to emit the events when a message is pushed in the queue and when a message is proceeded.
If you don't want to emit the events, you can put an empty block in the methods emit_event_message_queued
and emit_event_message_processed_to
.
/// Events emitted when a message is pushed in the queue
#[ink(event)]
pub struct MessageQueued {
pub id: u32,
pub data: Vec<u8>,
}
/// Events emitted when a message is proceed
#[ink(event)]
pub struct MessageProcessedTo {
pub id: u32,
}
impl rollup_anchor::EventBroadcaster for TestOracle {
fn emit_event_message_queued(&self, id: u32, data: Vec<u8>){
self.env().emit_event(MessageQueued { id, data });
}
fn emit_event_message_processed_to(&self, id: u32){
self.env().emit_event(MessageProcessedTo { id });
}
}
Implement the rollup_anchor::MessageHandler
trait to put your business logic when a message is received.
Here an example when the Oracle receives a message with the price feed.
impl rollup_anchor::MessageHandler for TestOracle {
fn on_message_received(&mut self, action: Vec<u8>) -> Result<(), RollupAnchorError> {
// parse the response
let message: PriceResponseMessage = Decode::decode(&mut &action[..])
.or(Err(RollupAnchorError::FailedToDecode))?;
// handle the response
if message.resp_type == TYPE_RESPONSE || message.resp_type == TYPE_FEED { // we received the price
// register the info
let mut trading_pair = self.trading_pairs.get(&message.trading_pair_id).unwrap_or_default();
trading_pair.value = message.price.unwrap_or_default();
trading_pair.nb_updates += 1;
trading_pair.last_update = self.env().block_timestamp();
self.trading_pairs.insert(&message.trading_pair_id, &trading_pair);
// emmit te event
self.env().emit_event(
PriceReceived {
trading_pair_id: message.trading_pair_id,
price: message.price.unwrap_or_default(),
}
);
} else if message.resp_type == TYPE_ERROR { // we received an error
self.env().emit_event(
ErrorReceived {
trading_pair_id: message.trading_pair_id,
err_no: message.err_no.unwrap_or_default()
}
);
} else {
// response type unknown
return Err(RollupAnchorError::UnsupportedAction);
}
Ok(())
}
}
Implement the meta_transaction::EventBroadcaster
trait to emit the events when a meta transaction is decoded.
If you don't want to emit the event, you can put an empty block in the methods emit_event_meta_tx_decoded
.
impl meta_transaction::EventBroadcaster for TestOracle {
fn emit_event_meta_tx_decoded(&self) {
self.env().emit_event(MetaTxDecoded {});
}
}
/// Events emitted when a meta transaction is decoded
#[ink(event)]
pub struct MetaTxDecoded {}
Here the final code of the Feed Price Oracle.
#![cfg_attr(not(feature = "std"), no_std, no_main)]
#[openbrush::implementation(Ownable, AccessControl)]
#[openbrush::contract]
pub mod test_oracle {
use ink::codegen::{EmitEvent, Env};
use ink::prelude::string::String;
use ink::prelude::vec::Vec;
use ink::storage::Mapping;
use openbrush::contracts::access_control::*;
use openbrush::contracts::ownable::*;
use openbrush::traits::Storage;
use scale::{Decode, Encode};
use phat_rollup_anchor_ink::traits::{
meta_transaction, meta_transaction::*, rollup_anchor, rollup_anchor::*,
};
pub type TradingPairId = u32;
/// Events emitted when a price is received
#[ink(event)]
pub struct PriceReceived {
trading_pair_id: TradingPairId,
price: u128,
}
/// Events emitted when a error is received
#[ink(event)]
pub struct ErrorReceived {
trading_pair_id: TradingPairId,
err_no: u128,
}
/// Errors occurred in the contract
#[derive(Encode, Decode, Debug)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum ContractError {
AccessControlError(AccessControlError),
RollupAnchorError(RollupAnchorError),
MetaTransactionError(MetaTransactionError),
MissingTradingPair,
}
/// convertor from MessageQueueError to ContractError
impl From<AccessControlError> for ContractError {
fn from(error: AccessControlError) -> Self {
ContractError::AccessControlError(error)
}
}
/// convertor from RollupAnchorError to ContractError
impl From<RollupAnchorError> for ContractError {
fn from(error: RollupAnchorError) -> Self {
ContractError::RollupAnchorError(error)
}
}
/// convertor from MetaTxError to ContractError
impl From<MetaTransactionError> for ContractError {
fn from(error: MetaTransactionError) -> Self {
ContractError::MetaTransactionError(error)
}
}
/// Message to request the price of the trading pair
/// message pushed in the queue by this contract and read by the offchain rollup
#[derive(Encode, Decode)]
struct PriceRequestMessage {
/// id of the pair (use as key in the Mapping)
trading_pair_id: TradingPairId,
/// trading pair like 'polkdatot/usd'
/// Note: it will be better to not save this data in the storage
token0: String,
token1: String,
}
/// Message sent to provide the price of the trading pair
/// response pushed in the queue by the offchain rollup and read by this contract
#[derive(Encode, Decode)]
struct PriceResponseMessage {
/// Type of response
resp_type: u8,
/// id of the pair
trading_pair_id: TradingPairId,
/// price of the trading pair
price: Option<u128>,
/// error when the price is read
err_no: Option<u128>,
}
/// Type of response when the offchain rollup communicates with this contract
const TYPE_ERROR: u8 = 0;
const TYPE_RESPONSE: u8 = 10;
const TYPE_FEED: u8 = 11;
/// Data storage
#[derive(Encode, Decode, Default, Eq, PartialEq, Clone, Debug)]
#[cfg_attr(
feature = "std",
derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout)
)]
pub struct TradingPair {
/// trading pair like 'polkdatot/usd'
/// Note: it will be better to not save this data outside of the storage
token0: String,
token1: String,
/// value of the trading pair
value: u128,
/// number of updates of the value
nb_updates: u16,
/// when the last value has been updated
last_update: u64,
}
#[ink(storage)]
#[derive(Default, Storage)]
pub struct TestOracle {
#[storage_field]
ownable: ownable::Data,
#[storage_field]
access: access_control::Data,
#[storage_field]
rollup_anchor: rollup_anchor::Data,
#[storage_field]
meta_transaction: meta_transaction::Data,
trading_pairs: Mapping<TradingPairId, TradingPair>,
}
impl TestOracle {
#[ink(constructor)]
pub fn new() -> Self {
let mut instance = Self::default();
let caller = instance.env().caller();
// set the owner of this contract
ownable::Internal::_init_with_owner(&mut instance, caller);
// set the admin of this contract
access_control::Internal::_init_with_admin(&mut instance, Some(caller));
// grant the role manager
AccessControl::grant_role(&mut instance, MANAGER_ROLE, Some(caller))
.expect("Should grant the role MANAGER_ROLE");
instance
}
#[ink(message)]
#[openbrush::modifiers(access_control::only_role(MANAGER_ROLE))]
pub fn create_trading_pair(
&mut self,
trading_pair_id: TradingPairId,
token0: String,
token1: String,
) -> Result<(), ContractError> {
// we create a new trading pair or override an existing one
let trading_pair = TradingPair {
token0,
token1,
value: 0,
nb_updates: 0,
last_update: 0,
};
self.trading_pairs.insert(trading_pair_id, &trading_pair);
Ok(())
}
#[ink(message)]
#[openbrush::modifiers(access_control::only_role(MANAGER_ROLE))]
pub fn request_price(
&mut self,
trading_pair_id: TradingPairId,
) -> Result<QueueIndex, ContractError> {
let index = match self.trading_pairs.get(trading_pair_id) {
Some(t) => {
// push the message in the queue
let message = PriceRequestMessage {
trading_pair_id,
token0: t.token0,
token1: t.token1,
};
self.push_message(&message)?
}
_ => return Err(ContractError::MissingTradingPair),
};
Ok(index)
}
#[ink(message)]
pub fn get_trading_pair(&self, trading_pair_id: TradingPairId) -> Option<TradingPair> {
self.trading_pairs.get(trading_pair_id)
}
#[ink(message)]
pub fn register_attestor(
&mut self,
account_id: AccountId,
ecdsa_public_key: [u8; 33],
) -> Result<(), ContractError> {
AccessControl::grant_role(self, ATTESTOR_ROLE, Some(account_id))?;
self.register_ecdsa_public_key(account_id, ecdsa_public_key)?;
Ok(())
}
#[ink(message)]
pub fn get_attestor_role(&self) -> RoleType {
ATTESTOR_ROLE
}
#[ink(message)]
pub fn get_manager_role(&self) -> RoleType {
MANAGER_ROLE
}
}
impl RollupAnchor for TestOracle {}
impl MetaTransaction for TestOracle {}
impl rollup_anchor::MessageHandler for TestOracle {
fn on_message_received(&mut self, action: Vec<u8>) -> Result<(), RollupAnchorError> {
// parse the response
let message: PriceResponseMessage =
Decode::decode(&mut &action[..]).or(Err(RollupAnchorError::FailedToDecode))?;
// handle the response
if message.resp_type == TYPE_RESPONSE || message.resp_type == TYPE_FEED {
// we received the price
// register the info
let mut trading_pair = self
.trading_pairs
.get(message.trading_pair_id)
.unwrap_or_default();
trading_pair.value = message.price.unwrap_or_default();
trading_pair.nb_updates += 1;
trading_pair.last_update = self.env().block_timestamp();
self.trading_pairs
.insert(message.trading_pair_id, &trading_pair);
// emmit te event
self.env().emit_event(PriceReceived {
trading_pair_id: message.trading_pair_id,
price: message.price.unwrap_or_default(),
});
} else if message.resp_type == TYPE_ERROR {
// we received an error
self.env().emit_event(ErrorReceived {
trading_pair_id: message.trading_pair_id,
err_no: message.err_no.unwrap_or_default(),
});
} else {
// response type unknown
return Err(RollupAnchorError::UnsupportedAction);
}
Ok(())
}
}
/// Events emitted when a message is pushed in the queue
#[ink(event)]
pub struct MessageQueued {
pub id: u32,
pub data: Vec<u8>,
}
/// Events emitted when a message is proceed
#[ink(event)]
pub struct MessageProcessedTo {
pub id: u32,
}
impl rollup_anchor::EventBroadcaster for TestOracle {
fn emit_event_message_queued(&self, id: u32, data: Vec<u8>) {
self.env().emit_event(MessageQueued { id, data });
}
fn emit_event_message_processed_to(&self, id: u32) {
self.env().emit_event(MessageProcessedTo { id });
}
}
impl meta_transaction::EventBroadcaster for TestOracle {
fn emit_event_meta_tx_decoded(&self) {
self.env().emit_event(MetaTxDecoded {});
}
}
/// Events emitted when a meta transaction is decoded
#[ink(event)]
pub struct MetaTxDecoded {}
}