Templates with Scaffold Stylus, OpenZeppelin 3.0 and testing with Forge Foundry, What's next for Stylusup? Interview with Philip Stanislaus
8th October, 2025
Hello everyone! In today’s post, we’ll be featuring the following:
An introduction to Scaffold Stylus 🧩, a template installing tool 📦 for getting started with Stylus 🧠, including using Chainlink’s data feeds 🔗 and VRF 🎲, and ERC20/ERC721 contracts 💰🎨.
OpenZeppelin 🧱 have had their contracts fully audited ✅ in their 3.0 release! Let’s do another round of discovery 🔍 of what’s available in their Stylus suite ⚙️. We’ll implement a proxy using their contract 🪞!
What’s next for Stylusup 🚀? Stylusup is about to see a huge change 🔄, with a new emphasis on community tooling 🤝 within the ecosystem 🌐. Stylusup will see a transition into community ownership 👥, and will be developed further with an ambition to become the community hub 🏗️ for Stylus 💫.
We’ve been lucky 🍀 to feature Philip Stanislaus 🎤 in an interview! Philip is the founder of Oak Security 🌳, a leading team building security tools 🔐 and performing audits 🧾. They’re working on building StylusPort 🛠️, a tool to convert Solana contracts ⚡ to Arbitrum Stylus 🌀 without friction ✨.
Read on, and as always, if you have any feedback, please share it here:
To recap, Stylus is a Arbitrum technology for building smart contracts in Rust, Typescript, Zig, and C. Stylus lets you build smart contracts that are 10-50x more gas effective than Solidity smart contracts.
StylusPort, a tool to convert Solana projects to Stylus, is looking for people to fill out a 5 minute survey. If you’re building in the Stylus and Solana ecosystem, be sure to fill out their Typeform!
Scaffold Stylus
Scaffold Stylus is a project creation tool for a number of contracts and frontends, including a simple ERC20, a ERC721, Chainlink Data Feeds, and Chainlink VRF. They also provide a block explorer for local development, and a Wagmi frontend for interacting with the contracts made using their platform!
There are several templates that you can extend from, including:
A ERC20 contract.
A ERC721.
A Chainlink price oracle frontend (and skeleton contract).
A Chainlink VRF contract.
Check out an example dApp here:
This is the default interface from creation! As you can see, it includes a block explorer built into the automatically created UI, which you can scan to see transactions that happened on the local network (or Sepolia).
It also includes a page to invoke methods on the contract you create:
Make sure to check the site out and give it a shot:
OpenZeppelin’s 3.0 release!
OpenZeppelin have made their 3.0 release, and it’s fully audited! Congratulations to everyone involved with this! I’m going to use this opportunity to recap what’s possible today, and what’s going to be possible in their future roadmap. For development, we’ll be testing using ArbOS-Foundry. ArbOS-Foundry is a fork of Foundry
In the 3.0 release
I really encourage everyone to read the release notes on this! Alisander (the dev that is powering a ton of the development on this) has introduced a TON of new contracts and features to the package. These include (and this is a selection):
Proxies, with a proxy supporting the beacon pattern, and a proxy supporting the transparent upgradeability pattern.
Sets for efficient storage, much like their Solidity equivalent.
Traits for various token receivers that can be used with the various token receiving specs out there! These include the function needed to be used in a callback for ERC721, which is used by any spec-conforming ERC721 implementation if your contract has code, and the one for ERC1155.
Using the new proxies
Let’s do a quick overview of how to make use of the OpenZeppelin proxies. We’ll create a new repository using cargo stylus new
, and after making some hygiene changes in line with my personal preference, we’ll cargo add openzeppelin-stylus
, and then we’ll begin to use Stylus trait inheritance features to build a virtual tollbooth contract in line with how I imagine this piece of Ice from the game Netrunner is like:
Netrunner is a cyberpunk asymmetric card game where a player, a hacker (“Runner”), works to breach the security of the servers of another player, a Corporation. The Corporation is able to deploy security programs called Ice to protect their servers, which the Runner must circumvent using hacking programs called Icebreakers. You can play online for free at Jinteki.net! We’re going to use OpenZeppelin’s upgradeable proxy to implement a virtual toolbooth, like the card shown above.
To invoke any method on the underlying implementation of a proxy, the user must pay 3 DAI. If the user fails to pay the 3 DAI, they will be disconnected from the contract with a revert!
We’ll be using the UUPS proxy pattern for this, which is a type of proxy pattern where the implementation contract is the one that’s responsible for its upgradeability. This makes a lot of sense, since the strong point of Stylus is its performance, which I visualise like this:
Where the blue line is Stylus, and the green line is Solidity. Source? It came to me in a fever dream. This is how I internalise my thinking with the setup and gas costs of Stylus.
For a proxy contract, you don’t want to pay that setup gas cost that you have to pay to warm the Stylus execution engine up (which is a fixed number), since your job is to send the calldata to another contract using a delegatecall, a very simple operation relatively. So upgrades might be something better supported by the implementation here, where maybe the contract observes a metamorphosis-like pattern (where the contract tears itself down when criteria is met) or something similar.
The contract
To use the new proxy code to make a proxy ourselves, we must implement several traits, and make them invokable using the public macro. These are functions to allow the user to set the owner of this contract, which we need to upgrade the proxy, and to invoke trusted functions.
We first create our contract storage like so:
#[entrypoint]
#[storage]
pub struct Storage {
pub fee_collector: StorageAddress,
pub token: StorageAddress,
pub fees_collected: StorageU256,
ownable: Ownable,
uups: UUPSUpgradeable,
}
We have three extra storage items beyond the two that we need for OpenZeppelin. One functions as a trusted role that has the power to invoke methods to collect fees collected in DAI. One stores the token address for DAI (that we could otherwise store as a constant if we so choose), and one contains a simple counter of fees collected, for demonstration purposes. It might be better to simply check the balance of the contract using the DAI ERC20 instead of maintaining a counter ourselves, but for our discussion, we’ll do it this way.
Following setting this in the storage of the contract, we can start to implement some functions for setup of the contract. Note that this contract does not have a constructor, which the implementation contract would have called! This calls the UUPS constructor, which prevents users from the implementation contract, who might have intended to call the proxy upgrade methods instead.
We implement our first implementation:
#[public]
#[implements(IOwnable, IUUPSUpgradeable, IErc1822Proxiable)]
impl Storage {
pub fn init(
&mut self,
owner: Address,
fee_collector: Address,
token: Address,
) -> Result<(), Vec<u8>> {
self.uups.set_version()?;
self.ownable.constructor(owner)?;
self.fee_collector.set(fee_collector);
self.token.set(token);
Ok(())
}
}
This method invokes trusted functions on the storage items for the UUPS implementation (that does the fallback method) to set storage that indicates it should be impossible to invoke the methods responsible for proxy setup. It sets a storage slot intended for the storage of the logic contract, which has a nice side effect of preventing us from calling this setup function more than once. We implement the rest of the traits:
#[public]
impl IUUPSUpgradeable for Storage {
#[selector(name = “UPGRADE_INTERFACE_VERSION”)]
fn upgrade_interface_version(&self) -> String {
self.uups.upgrade_interface_version()
}
#[payable]
fn upgrade_to_and_call(
&mut self,
new_implementation: Address,
data: stylus_abi::Bytes,
) -> Result<(), Vec<u8>> {
self.ownable.only_owner()?;
self.uups.upgrade_to_and_call(new_implementation, data)?;
Ok(())
}
}
In the code that does the upgrade and call, we could implement some additional access control there. In the above, it uses the ownable code to check if the sender is the owner of the contract. We need to implement some functions that ownable expects to support it fully. These functions implement the basics in OZ contracts:
#[public]
impl IOwnable for Storage {
fn owner(&self) -> Address {
self.ownable.owner()
}
fn transfer_ownership(&mut self, new_owner: Address) -> Result<(), Vec<u8>> {
Ok(self.ownable.transfer_ownership(new_owner)?)
}
fn renounce_ownership(&mut self) -> Result<(), Vec<u8>> {
Ok(self.ownable.renounce_ownership()?)
}
}
Following the implementation code doing access control and setting slots, we need to implement the contract that reads the slot before a delegatecall. Since the implementation manipulates the storage slots, our proxy needs to check the standard location to know where to call into.
We can implement a simple Solidity contract:
bytes32 constant SLOT_LOGIC = bytes32(uint256(keccak256(’eip1967.proxy.implementation’)) - 1);
contract Proxy {
address private feeCollector;
IERC20 private token;
uint256 private collected;
uint256 private feeAmount;
event TollPaid(
address indexed payer,
uint256 indexed amount
);
constructor(
address _owner,
address _feeCollector,
address _token,
uint256 _feeAmount,
address _impl
) {
bytes32 slot = SLOT_LOGIC;
assembly {
sstore(slot, _impl)
}
(bool success,) = _impl.delegatecall(abi.encodeWithSelector(
IImpl.init.selector,
_owner,
_feeCollector,
_token,
_feeAmount
));
require(success);
}
fallback() external {
address impl;
bytes32 slot = SLOT_LOGIC;
assembly {
impl := sload(slot)
}
require(token.transferFrom(msg.sender, address(this), feeAmount));
emit TollPaid(msg.sender, feeAmount);
collected += feeAmount;
(bool success, bytes memory data) = impl.delegatecall(msg.data);
if (data.length > 0 && !success) {
assembly {
revert(add(data, 0x20), mload(data))
}
} else {
require(success);
if (data.length > 0) {
assembly {
return(add(data, 0x20), mload(data))
}
}
}
}
}
This simply forwards all calldata to the implementation address at the standard EIP1967 slot (computed at the top of the contract). It also calls the init function inside the implementation during the contract’s construction. It’s the code that actually enforces the tollbooth collection during the fallback operation, using a transferFrom each time it’s called. It also emits a handy event for monitoring how the tollbooth is performing.
The issue with this code is that it’s a pain to approve before each invocation of the contract. It might be better to have a calldata forwarder, or to restructure the caldata to include a permit blob beforehand. You could imagine that this code is useful for an administrative task though, or something like those “trick the AI to send you something” challenges.
Note how during the setup, we don’t actually set any values ourselves apart from the storage slot? This is because Stylus has 1-to-1 equivalence with the EVM, down to the storage layout! So our implementation contact can be fully responsible for its upgrades, as well as how the proxy is performing.
Testing Arbitrum Stylus with Forge Foundry
In today’s post, we’ll be testing using ArbOS-Foundry, a project developed by the team at iosiro. It lets us test Solidity and Stylus side by side, as first class citizens! It’s a fork of Foundry with the hope of eventually providing that experience there. We’ll be testing that the proxy is taking fees every time someone calls the implementation. We’ll be doing a later post on this, since ArbOS-Foundry is such a great opportunity for the space.
After pulling down the repo and installing everything (I just ran make build
, which created the executables arbos-forge
and arbos-cast
in the target debug directory), I created this test:
import {SLOT_LOGIC, Proxy} from “../src/Proxy.sol”;
interface IArbFoundry {
function deployStylusCode(string calldata artifact) external returns (address);
}
interface IProxy {
function upgradeToAndCall(address newImpl, bytes memory data) external;
}
interface IHello {
function hello() external returns (string memory);
}
contract TestErc20 {
bool public wasSpent;
function transferFrom(address, address, uint256) external returns (bool) {
wasSpent = true;
return true;
}
}
contract World {
function world() external returns (string memory) {
return “World!”;
}
function proxiableUUID() external returns (bytes32) {
return SLOT_LOGIC;
}
}
contract TestProxy is Test {
TestErc20 erc20;
address impl;
Proxy proxy;
address impl2;
function setUp() public {
erc20 = new TestErc20();
impl = IArbFoundry(address(vm)).deployStylusCode(”oz-proxies.wasm”);
proxy = new Proxy(address(this), address(this), address(erc20), 1e6, impl);
impl2 = address(new World());
}
function testImpl() public {
assertNotEq(address(0), impl);
assertEq(”Hello!”, IHello(impl).hello());
}
function testProxy() public {
assert(!erc20.wasSpent());
assertEq(”Hello!”, IHello(address(proxy)).hello());
assert(erc20.wasSpent());
IProxy(address(proxy)).upgradeToAndCall(impl2, “”);
assertEq(”World!”, World(address(proxy)).world());
}
}
This tests that we can upgrade, that we can invoke the functions on the implementation, and that we can use the proxy. You might notice that we implement a function called proxiableUUID
in the second implementation. OZ give us some sanity checks, and this function returns the slot that’s responsible for the logic. This is so that the UUPS proxy knows for certain that we know what we’re doing. Once we do the upgrade in this example, we can’t go back and do another upgrade!
Thanks to the implementation-led mutability of the deployment, we can have code that might mutate itself to become immutable after a release cycle or an audit for example. This code captures that phenomenon!
Before we run our tests, we’ll first create a Makefile to build our code before testing:
oz-proxies.wasm: $(shell find src -name ‘*.rs’)
@rm -f oz-proxies.wasm
@cargo build --target wasm32-unknown-unknown --release
@wasm-opt \
--dce \
--rse \
--signature-pruning \
--enable-bulk-memory \
--strip-debug \
--strip-producers \
-Oz target/wasm32-unknown-unknown/release/oz_proxies.wasm \
-o oz-proxies2.wasm
@wasm2wat oz-proxies2.wasm >oz-proxies.wat
@wat2wasm oz-proxies.wat >oz-proxies.wasm
@rm -f oz-proxies2.wasm oz-proxies.wat
Why do this? A step is needed before using the raw wasm output with arbos-foundry right now. The node does a processing step of using the wasm2wat/wat2wasm toolchain to compile the WASM code to a SEXP-like representation, the WAT format, then back. I’m assuming this is needed to strip some bits of code as a side effect, though it’s not known to me what the literal need is. We can make a testing script that invokes the Makefile and the tests like so:
#!/bin/sh -e
make -B
arbos-forge test $@
In our code creation pipeline, we first create the code using a Makefile that runs Cargo to compile this wasm blob. This test then picks up the file, deploys it, then constructs the Proxy. With a fake ERC20 that checks if the token was fake transferred. Cool!
What’s next for Stylusup?
Stylusup is undergoing a huge change! Stylusup will transition from being a on-ramping page for Stylus to becoming a community ecosystem landing page. The development of the new page is still in progress, but we’ve created a new community organisation to take ownership of it! You can see it here: https://github.com/stylus-developers-guild
Currently, we have the following members:
Alex (this author), CTO of Fluidity Labs (building Superposition), https://fluiditylabs.io
Tolga, Founder of The Wizard, https://thewizard.app
David, Senior Product Manager at Offchain Labs, https://www.offchainlabs.com/
The goal is to transition the site to a new format where we can aggregate projects from the community in one place, with a transparent listing process based on pull requests. We have something cooking, and I can’t wait to share it with you! A very appreciate thank you to Tolga and David for supporting this!
Interview with Philip Stanislaus
Who are you, and what do you do?
I’m Philip Stanislaus, co-founder and Managing Director of Oak Security.
My journey began in traditional software engineering, freelancing for over a decade and co-founding several startups. I later transitioned into blockchain, working as a developer at Centrifuge and architecting projects like Snowfork. I then co-founded Oak Security to help Web3 projects build safely and confidently by offering security audits, penetration testing, training, and other related services.
Recently, we launched StylusPort with Range, a framework that simplifies migrating Solana programs to Arbitrum’s Stylus environment. My focus is enabling developers to innovate while maintaining security, efficiency, and interoperability across ecosystems.
What Web3 ecosystems have you worked in?
I’ve worked in Ethereum, Arbitrum, Solana, Cosmos, Polkadot, and Flow. Currently, Arbitrum is a key focus because of its growing adoption, developer-friendly environment, and the ability to deploy Rust-based smart contracts via Stylus. StylusPort helps Solana developers bring their Rust projects into Arbitrum without rewriting in Solidity, bridging ecosystems efficiently.
What are your impressions of working with Stylus, good and bad?
Stylus is a breakthrough for Rust and C++ developers wanting to build without EVM constraints. Its low-cost computation, strong tooling, and Ethereum liquidity enable use cases that were previously out of reach.. Moving from another ecosystem to Arbitrum Stylus is not straightforward, though. We are building StylusPort to address that by providing clear guidance and AI-assisted tooling to reduce friction. Overall, Stylus is a vibrant, exciting ecosystem with huge potential.
What software do you use?
I primarily use Visual Studio Code with Rust and Solidity extensions. For project management and collaboration, Google Docs/Sheets and GitHub are essential. During our security work, we use numerous tools, like static analyzers, fuzzers, formal verification tools, and LLMs.
What hardware do you use?
I connect a MacBook to a remote machine using VSCode’s Remote Development using SSH. Our LLMs run on more powerful remote machines with GPU support.
How can we get in touch with you?
You can reach me on LinkedIn, X/Twitter, or via email at philip@oaksecurity.io. I enjoy connecting with developers and founders who are building cross-chain projects, securing protocols, or exploring the adoption of Stylus and Arbitrum. If you are interested in using or giving feedback on StylusPort, please get in touch!
What’s a piece of advice you’d give someone new to Web3?
Focus on both product market fit and security from the beginning. Web3 is exciting, but lasting success comes from solving real problems and prioritizing safety. Focus on creating value and keeping your users’ funds and your reputation safe, not just following trends.
To get in touch with the Oak Security team, you can check their website at https://www.oaksecurity.io/.
Stylus Saturdays is brought to you by… the Arbitrum DAO! With a grant from the Fund the Stylus Sprint program. You can learn more about Arbitrum grants here: https://arbitrum.foundation/grants
Follow me on X: @baygeeth and on Farcaster!
Side note: I develop Superposition, a defi-first chain that pays you to use it. You can check our ecosystem of dapps out at https://superposition.so!
None of this is real. If you’re reading this, you’re in a coma, and you need to wake up! We love you.