Rust for smart contracts book, on-chain randomness and price oracle using Chainlink with Stylus (part 1), Alisander from OpenZeppelin
Stylus Saturdays, 3rd March 2023
Hello again! A bit of a delayed post, being at ETH Denver threw my schedule off a little.
Welcome to another Stylus Saturdays post! To recap: Stylus lets you build smart contracts on Arbitrum using any programming language that compiles to WASM. Your contracts are one-to-one compatible with the EVM, even down to the storage backend.
In today’s episode, we’ll discuss how to build a vending machine with Arbitrum Rust Stylus and Chainlink VRF and price feeds, and feature Alisander Qoshqosh from OpenZeppelin’s Stylus team. I’m excited to share that I’ve been writing a book on developing Rust-powered smart contracts:
You can find it at https://rust-for-smart-contracts.com! Please help me out a lot, and if you’re interested, sign up for the waitlist at:
https://getwaitlist.com/waitlist/25549
It introduces how blockchains work from an end developer point of view (beginning with some foundational distributed thinking and cryptography skills), how a smart contract works and then finally how to build and test one from the ground up for Arbitrum, Solana, and NEAR.
It’s also a work in progress, with only the first chapter mostly written up (and needing review), but I enjoy building in public, so please interact with it by providing suggestions if you’d like. I’ll also send everyone who waitlists the book a special Superposition NFT as thanks.
I’m also pleased to share that our new domain https://stylus-saturdays.com will help with our search presence, and will afford me the chance to host a subdomain of resources.
Using Chainlink Price Feeds and Chainlink VRF with Rust Arbitrum Stylus (part 1)
Using Chainlink’s VRF with Arbitrum Stylus is super easy! Being unable to implement an interface may be confusing for some, but fear not: it’s as simple as exporting a specific function in your entrypoint code.
For part 1 of this post, we’re going to implement the entrypoint for users to call, then conclude with the callback in the next post, including some approaches for testing code which includes hard to mock behaviour with a manual gas estimation. Since this is a development journey (in contrast to our completed posts of a similar nature in the past), this code is largely untested.
I visited Japan recently (for the second time), and since middle school, I’ve had an interest that comes to life when I’m abroad looking at vending machines. Not the kind that overrides my personality and makes me a stereotype, but the kind where I take photos if I see one. I wrote a lengthy school essay that really opened my eyes at the time, there’re lots of different vending machines! Wikipedia was only just becoming a thing and we were learning how to do primary and secondary research.
Together, let’s implement a NFT vending machine contract that uses a Chainlink Verifiable Random Function (VRF) to get a random word to seed a random number generator. We’ll use the random word to pick a NFT for a requesting user after using a Chainlink Price Feed to convert the ETH supplied to USD for selecting a NFT tranche to send.
The NFTs are stored in a vector that goes one way, with no pricing from the contract itself in the form of buckets that you might see in a concentrated AMM-like structure.
We’ll take a flat fee from users who participate in this system to power our VRF using Chainlink’s 2.5 direct funding model. This lets us avoid having to set up a subscription elsewhere, and we can focus on taking a fee from users to service the requests instead. We’ll refund every user based on the share of asks and the flat fee.
The user (and technical) story is like this for funding:

So, a user goes to spend their ETH, paying a flat fee for doing so. The funds are locked up until a Chainlink VRF request is fulfilled with a callback, at which point Chainlink’s price feeds are accessed to get the value of ETH at that time. At that point, the NFT tier is chosen from the vector that was established earlier, and we send the user their NFT, or a refund. We assume that Chainlink will absolutely call this function (and that in doing so we’ve accurately predicted the uppermost ETH to spend for the callback).
Implementing some code
First, let’s establish some storage. In a production-facing application, you might prefer to use a special number pattern (think sqrt price x96 if you’re like Uniswap) based on some configuration local to the contract to bucket price ranges which you then store with a mapping, but in our usecase, we’re going to be taking a very simple view of this:
This code is a series of stored structures, loaded by a vector and map combination. Why don’t we storage tag the StorageQueue? The address and u96 combination is storeable in a EVM word. This affords us the efficiency of unpacking a word to access the amount a user has deposited, as opposed to a pair of SLOADs.
For the other fields inside this storage structure, we’re not going to attempt to pack them, as these accesses are virtual anyway. When you have a recursively stored keyed storage value (ie, a map or a vector), Stylus will keccak256 to find the location of the offset for the lookup so it won’t be implicitly loaded when you load a storage slot, only on demand.
Making requests of Chainlink is simply calling a method, which then triggers a callback. To do so, first we define a entrypoint function for users to call.
In the new version of Stylus, errorneous returns from function calls in the new Call functions return a enum type. This type is either the return data from the call, or a type that indicates a decoding failed.
We first define a macro to simplify unpacking this, and we use the sol! macro to load several error types from a Solidity file:
use stylus_sdk::{alloy_sol_types::sol, stylus_core::calls::errors::Error};
pub(crate) fn unpack_err(x: Error) -> Vec<u8> {
match x {
Error::Revert(x) => x,
_ => unimplemented!()
}
}
#[macro_export]
macro_rules! unpack_on_err {
($rd:expr, $conv:ident) => {{
use stylus_sdk::alloy_sol_types::SolError;
$rd.map_err(|x| $conv{_0: $crate::errors::unpack_err(x).into()}.abi_encode())
}};
}
#[macro_export]
macro_rules! revert {
($err:ident) => { return Err($err{}.abi_encode()); }
}
sol!("./src/IErrors.sol");
pub use IErrors::*;
The sol macro lets us use:
Which we’ll bring into scope as we use this code.
With that macro sorted, we can start to write some code like:
use crate::errors::*;
sol!("./src/IVRFV2PlusWrapper.sol");
// Calls "calculateRequestPrice" from Chainlink's IVRFV2PlusWrapper.
pub fn calculate_request_price_native(
access: &dyn CallAccess,
addr: Address,
callback_gas_limit: u32,
num_words: u32,
) -> Result<U256, Vec<u8>> {
Ok(U256::from_le_slice(&unpack_on_err!(
access.static_call(
&Call::new(),
addr,
&IVRFV2PlusWrapper::calculateRequestPriceNativeCall {
_callbackGasLimit: callback_gas_limit,
_numWords: num_words,
}
.abi_encode()
),
ErrChainlinkVRF
)?))
}
This code neatly makes a request using the calling context in the new Stylus release, then unpacks it to a vector containing bytes if something goes wrong. Before we precede any request to Chainlink’s VRF, we must estimate using their code the fee cost in the native asset. This is due to us using their native payment path, which you can read more about here: https://docs.chain.link/vrf/v2-5/overview/direct-funding
The sol macro lets us seamlessly interleave Solidity code with our Rust. This makes accessing events simple, as we don’t need to define them ourselves, and it facilitates classical EVM tooling (think Alloy). With a function that estimates the cost, we can start to request the random word itself with a function like this:
pub fn request_random_words_in_native(
access: &dyn CallAccess,
addr: Address,
value: U256,
callback_gas_limit: u32,
request_confirmations: u16,
num_words: u32,
extra_args: Bytes,
) -> Result<U256, Vec<u8>> {
Ok(U256::from_le_slice(&unpack_on_err!(
access.call(
&Call::new().value(value),
addr,
&IVRFV2PlusWrapper::requestRandomWordsInNativeCall {
_callbackGasLimit: callback_gas_limit,
_requestConfirmations: request_confirmations,
_numWords: num_words,
extraArgs: extra_args,
}
.abi_encode()
),
ErrChainlinkVRF
)?))
}
This unpacks the function requestRandomWordsInNativeCall.
We can start to finally build our entrypoint code for locking up users:
#[payable]
pub fn lockup(&mut self, recipient: Address) -> Result<U256, Vec<u8>> {
require!(!self.version.is_zero(), ErrNotSetup {});
require!(
!recipient.is_zero(),
ErrInvalidRecipient {
sender: self.vm().msg_sender()
}
);
let value = self.vm().msg_value();
require!(value > U256::ZERO, ErrNoValue {});
require!(value <= MAX_U256_VALUE, ErrTooMuchValue {});
// Prevent people from using this if there aren't any NFTs to distribute.
let has_nfts = (0..self.levels.len())
.find(|i| self.levels.get(*i).unwrap().nfts_distributeable.len() > 0)
.is_some();
require!(has_nfts, ErrNoNfts {});
if !self.chainlink_vrf_pending.get() {
let fee = self.estimate_fee()?;
self.chainlink_vrf_fee.set(fee);
let ticket_no = chainlink_vrf_call::request_random_words_in_native(
self.vm(),
CHAINLINK_VRF_ADDR,
fee,
ESTIMATED_CALLBACK_LIMIT,
CHAINLINK_VRF_CONFIRMATIONS,
CHAINLINK_NUM_WORDS,
Bytes::new(),
)?;
stylus_core::log(
self.vm(),
RandomnessRequested {
ticketNo: ticket_no,
},
);
self.chainlink_vrf_pending.set(true);
}
let value = value
.checked_sub(self.chainlink_vrf_fee.get())
.ok_or(ErrCheckedSub {}.abi_encode())?;
// Push the user's request for the NFT into the queue.
let ticket_no = U256::from(self.queue.len());
stylus_core::log(
self.vm(),
LockedUpTokens {
recipient,
amount: value,
},
);
self.queue.grow().set(pack_queue_item(value, recipient));
Ok(ticket_no)
}
This payable function takes ETH, checks that we have NFTs to distribute, checks if we have a Chainlink reply pending, and if we don’t, makes the call out to Chainlink using the function we introduced earlier. It uses a hardcoded value in immutables for some of the gas fields, which we’ll expand on in the next post properly. Following that, it adds the fee to the value, breaking explicitly if a user supplied too little!
It then grows out the storage internally of the queue with a packed word and returns the location in the internal storage to the user. And that’s it for now! Completion (and perhaps some live coding) in the next post on this topic.
Interview with Alisander Qoshqosh from OpenZeppelin
I’m lucky to have had the opportunity to spotlight Alisander from OpenZeppelin! Alisander is an open source developer at OpenZeppelin, working on openzeppelin-stylus.
Who are you?
I’m Alisander Qoshqosh, Open Source Developer at OpenZeppelin.
What’s your project?
Currently I work on openzeppelin-stylus, openzeppelin-crypto and motsu crates.
OpenZeppelin’s Stylus projects are super useful for developers in the ecosystem. Their code at https://github.com/OpenZeppelin/rust-contracts-stylus supports developers in quickly bringing up standard patterns, including working with ERC20 contracts. Their contracts are fully audited, which takes the edge off developing with Stylus for the first time.
OpenZeppelin recently found that Poseidon hashing with Stylus is 90x cheaper than with the ordinary EVM environment: https://blog.openzeppelin.com/poseidon-go-brr-with-stylus-cryptographic-functions-are-18x-more-gas-efficient-via-rust-on-arbitrum
Their openzeppelin-crypto crate simplifies the process of using cryptography on-chain with the same level of security and reusability that you might benefit from with the rest of their code. I couldn’t see whether their crypto package includes the aforementioned Poseidon hashing, but it definitely supports merkle tree proving.
Could you tell us more about your background before web3?
Before web3 I was a backend developer (mostly C#, Rust and Java) for large enterprise projects in telecommunications industry.
Have you worked in other ecosystems aside from the EVM?
I haven’t worked professionally in other ecosystems aside Ethereum.
What were the opportunities for OZ with Stylus?
We want to replicate the success of our Solidity library and make sure Stylus developers have access to secure, efficient and easy to use primitives to build their applications.
What were the joys of working with Stylus?
It is always a joy to write code on Rust. And Stylus brings rust to EVM. Like it.
How do you see the Stylus ecosystem evolving?
Before Stylus, it was actually hard to work with the inheritance problem, the lack of constructors, and a lack of testing framework. But good to see, that the last issue is already addressed. And other features are coming soon in the stylus-sdk 0.9.0. So yeah, I see a big leap forward!
How can we get in touch with you?
Telegram: https://t.me/qalisander
Stylus Saturdays is brought to you by… Arbitrum! Thanks to a special grant from the foundation.
This post is powered by… Arbitrum! With a grant from the Fund the Stylus Sprint program. You can learn more about Arbitrum grants here: https://arbitrum.foundation/grants
Join the Stylus Devs DAO: https://discord.gg/eTRt3r3F
Follow me on X: @baygeeth
Side note: I develop Superposition, a defi-first chain that pays you to use it. You can check our dapps out at https://superposition.so