Introducing Bobcat SDK, a TINY (3x smaller than Stylus SDK) community owned SDK for Rust Arbitrum Stylus
15th October 2025
Hello everyone! I have a smaller post here today, as I’m working on Orderbookkit. I’m excited to share that I’ve developed a new SDK for Stylus that’s 3x smaller than the default SDK with the counter example! We wanted to build an extremely small SDK for ourselves, since we know that this is one of the main pain points in the ecosystem right now. We’ve used it in our upcoming rollup Superposition Passport for a 10kb gas saving with a simple drop in.
The standard Stylus SDK is one of the most expressive kits out there for development. It’s super user friendly, making it easy for pros and juniors alike to onboard into the world of Stylus. For us, as we’ve settled into development and figured out our own strategies, we found it interfering with our development, thanks to the sheer amount of features.
We accomplished gas savings by reducing the amount of abstractions in the SDK, using host features for checked math, not using the allocator by default, and more! As a development practice, I’m also working on aggressively regression testing feature release against a suite of examples, to avoid codesize creep. The goal with this SDK will be to mainly get the code size lower. Together, let’s do a quick overview of the features, and how to use it yourself!
No native types except for signed and unsigned 256 bit numbers
We don’t support any numbers natively except the U256 and I256 types. These are implemented simply, as a slice that’s assumed to be big endian. The default SDK leverages ruint through alloy to support EVM words, and this is can be wasteful, as there are several optimisations baked directly into ruint for performance. These result in more code being generated that we don’t benefit from (I assume), given the wasm backend.
Using the host features to do checked math
Another way we reduce codesize is by using the host provided external functions to do math. These functions are:
unsafe extern “C” {
fn math_div(x: *mut u8, y: *const u8);
fn math_mod(x: *mut u8, y: *const u8);
fn math_add_mod(a: *mut u8, b: *const u8, c: *const u8);
fn math_mul_mod(a: *mut u8, b: *const u8, c: *const u8);
}
By using these, we can implement checked math in a super codesize effective way. For example:
pub fn checked_mul(x: &U, y: &U) -> Option<U> {
if x.is_zero() || y.is_zero() {
return Some(U::ZERO);
}
if x > &(U::MAX / *y) {
None
} else {
let z = x.mul_mod(y, &U::MAX);
if z.is_zero() {
Some(U::MAX)
} else {
Some(z)
}
}
}
For wrapping math, it’s a different story, so we provide it as a code implemented function. But, this is provided as a constant-time function, so it could be used for constant time code that’s not provided at runtime. In practice, developers developing for the EVM will likely use checked math everywhere (except for fees). Codesize-minded developers can use saturating math if they really want to save space, since that will use the checked code.
No allocator required (and many ways of doing calls)
Calls are made to be dirt simple, with plenty of different ways to make the call and access the calldata. Safe variants are available for every type of call, checking the codesize of the target before execution. There are different return types with different levels of abstraction, providing ways for programmers to reduce codesize or improve devex however they want.
No allocator is required to use bobcat_sdk, saving lots of space in the code output! All the outgoing calls can be performed using slices for the returndata, or vectors. This approach is used to let the user pick if they want to read simple boolean data, or to unpack an entire slice. This is implemented throughout the codebase.
Interfaces made easy
Several interfaces are available for operations including EIP20, EIP2612. More will be made available. It makes calling easy. This is a contract’s entire codebase:
#![no_main]
#![no_std]
use bobcat_sdk::{
call::static_call_slice,
entry::*,
interfaces::eip20::make_fn_allowance,
maths::U,
};
#[unsafe(no_mangle)]
pub unsafe extern “C” fn user_entrypoint(args_len: usize) -> usize {
let args = &read_args_safe!(args_len, { 32 * 3 + 4 });
let (contract, owner, spender) = read_word_slices!(&args[4..], 3);
write_result_exit_call!(static_call_slice::<32>(
contract.into(),
&make_fn_allowance(owner.into(), spender.into()),
u64::MAX,
0,
))
}
This function simply calls a contract to ask for its allowance, returning the results. This contract is 4kb large!
Lots of versions of the calling interface are available:
safe_call_bool_opt(address, &make_fn_transfer(recipient, amt))
.unwrap_or(Error::BadTransfer)
Which would check the contract’s codesize before making the call, only returning if the contract either returned nothing but code exists, or returning something but true. Good for backwards compatibility with ERC20? It should play nice with the interfaces!
Exotic types of storage accessors
Many types of storage accessor functions are made available, also with access to transient storage. These can be used to implement code that manipulates the underlying state in a way that’s more application-centric. An exchange function is also available, letting the user “check and set” like an atomic interaction. Each math operation is available over the storage.
Proxies made easy
Several proxies are made available in the SDK that make new contract deployment easy. These include a proxy that simply forwards calldata to an address in the contract bytecode, one that checks a storage slot for sending, and one that calls the implementation
function on a beacon to get the address.
These were implemented in Huff to reduce the code overhead, and are simple to use:
#![no_main]
#![no_std]
use bobcat_sdk::{
cd::{const_keccak_sel, read_word_slices},
create::create1_slice,
entry::*,
maths::U,
proxy::make_beacon_proxy,
};
const SEL: [u8; 4] = const_keccak_sel(b”deploy(address)”);
#[unsafe(no_mangle)]
pub unsafe extern “C” fn user_entrypoint(args_len: usize) -> usize {
let args = &read_args_safe!(args_len, { 32 + 4 });
if args[..4] != SEL {
return 1;
}
let beacon = read_word_slices!(args, 1);
let r = create1_slice::<1024>(&make_beacon_proxy(beacon.into()), U::ZERO);
write_result_exit_create!(r)
}
This contract consumes 16kb. The macro that returns the data doesn’t work with the Result type, instead unpacking a tuple and checking a boolean flag. There are several return macros provided that work with different shapes.
Coming soon
Currently, storage access is function driven. We’ll be releasing a storage guard similar to what’s in the main SDK in the future. We’ll be porting over the examples form the Stylus SDK as well, and focusing on improving the codesize profile with inline optimisations where we can. We also need to have muldiv implemented, as well as the rounding up equivalent, for serious use. We’ll be working on supporting the example in the README, at which point, we’ll be considered complete! Stay aware of this space!
How do you test?
No testing functions are available to mock chain state. That’s why it’s important to combine it with another project, arbos-foundry, for on-chain end to end testing! We found that in practice, the only kind of development we were doing was mocking local functions. We’ll continue to do our testing development in Rust for our core algorithms. We’ll do a writeup on how this can be used in the future, though last’s post also discussed this topic.
Read the README!
Make sure to read the README to learn more! If you wind up using the SDK, drop me a message.
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!
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