Our custom AI translated a Solana Multisig to Stylus. Read what (didn’t) work.
Solana programs are Rust. Arbitrum Stylus contracts are Rust. Translating a Solana contract to run on the EVM is still non-trivial.
We built StylusPort to close that gap: an open-source MCP server paired with a 13-chapter migration handbook for Solana-to-Stylus work.
Stylus port has the power to overcome what separates Solana and the EVM: The execution and data models of Solana and Stylus diverge in critical ways: Solana accounts vs EVM storage, PDAs vs contract addresses, Borsh vs ABI encoding, CPI vs external calls, and compute units vs gas.
We equipped Claude Opus 4.6 with StylusPort and ported a Solana Multisig to Stylus. The input was Coral’s production multisig program (roughly 300 lines of Anchor Rust). The output was a Stylus contract implementation with 23 unit tests.
Read below what worked, what failed, and what still needs human review. The short version: structured retrieval and constrained workflows produce a strong starting point, but they do not replace audit-grade engineering.
ℹFor readers unfamiliar with MCP: Model Context Protocol is an open standard that lets AI assistants use external tools and knowledge.
What is StylusPort?
StylusPort has three components.
The Handbook. Thirteen mdbook chapters covering migration patterns: program structure, state storage, access control, external calls, native tokens, ERC20/721, errors and events, a full Bonafida Token Vesting case study, testing/debugging, gas optimization, and security. Each chapter includes side-by-side Solana and Stylus examples in both Anchor and “Native” flavors.
The MCP Server. A Rust binary exposing four tools, thirteen resources, and two prompts over stdio. It works with Claude Code, Cursor, OpenCode, and other MCP-compatible harnesses.
The Prompts. Two structured workflows with defined steps:
Plan --- analyze the Solana program, search the handbook, then produce an 11-section migration plan with architecture mapping tables, a risk register (minimum eight items), implementation phases, and a test plan.
Execute --- read the plan, implement phase by phase, verify WASM compilation, run tests, and produce a completion summary.
The four tools
detect_solana_program_kind --- reads a Cargo.toml and returns “anchor” or “native” to determine migration strategy.
search_handbook --- BM25 full-text search across all 13 chapters, returning ranked resource URIs.
generate_stylus_contract_cargo_manifest --- produces a Cargo.toml with pinned dependencies (stylus-sdk = “=0.9.0”, alloy-primitives = “=0.8.20”, motsu = “0.10.0”), WASM target config, and release optimizations.
generate_stylus_contract_main_rs --- produces the ABI export entrypoint boilerplate.
The target: Coral Multisig
Coral (formerly Serum) Multisig is a production Solana governance contract. It is compact (about 300 lines in one lib.rs) but still exercises migration-critical patterns.
It implements an M-of-N multisig wallet. A group of owners collectively approve and execute arbitrary Solana transactions. The key concepts:
Multisig account: owners list, approval threshold, and an owner_set_seqno (a version counter that invalidates all pending transactions whenever the owner set changes).
Transaction account: target program, instruction data, an approval bitmap tracking which owners have signed, and a did_execute flag preventing replay.
PDA signer: a Program Derived Address that signs for cross-program invocations during execution.
Self-governance: set_owners and change_threshold can only be called through the multisig’s own execute_transaction, creating a recursive self-call pattern.
It uses Anchor, multiple related account types, CPI with PDA signing, layered access control, and state invalidation logic, while remaining small enough to verify by hand.
The migration in two commands
Step 1: plan
We invoked the Plan prompt inside Claude Code.
Discovery: Claude scanned for Cargo.toml, read programs/multisig/src/lib.rs, then ran detect_solana_program_kind and got “anchor”, which routes it to the Anchor-flavored handbook sections.
Handbook research: Five parallel search_handbook calls followed; each returned a ranked list of chapters to read.
“storage state accounts mapping Solana to Stylus”
“access control authorization signers owners”
“CPI external calls cross-program invocation”
“serialization data layout borsh anchor”
“errors events logging revert”
Note: targeted search helps avoid context-window overload, which can reduce output quality.
Then it read seven handbook chapters via their MCP resource URIs: state-storage, access-control, external-calls, errors-events, security-considerations, testing-debugging, and program-structure.
Boilerplate generation: Both generators were called to produce a Cargo.toml and main.rs with pinned dependency versions.
Compared with ad hoc prompting (or Skills alone), the MCP server reduces unnecessary non-determinism by generating standardized boilerplate.
Plan output: Claude wrote a 551-line plan.md covering all eleven required sections. Key sections included:
The architecture mapping tables captured how each Solana account type becomes Stylus contract storage:
The risk register identified ten risks with concrete mitigations:
Reentrancy. Solana’s runtime prevents reentrancy by default. The EVM does not. Mitigation: set did_execute = true before the external call (checks-effects-interactions pattern).
Owner index drift. If the owner set changes between transaction creation and execution, signer indices become meaningless. Mitigation: the owner_set_seqno mechanism, carried over directly from the Solana implementation.
Self-call authorization. Solana uses PDA signing. Stylus needs msg_sender() == contract_address(). Governance functions must be unreachable from external callers.
You can see the generated plan in its entirety here.
Step 2: execute
We invoked the Execute prompt. Claude read the plan and began implementing it phase by phase.
Phase 1 - Project Setup: Created Cargo.toml, src/main.rs, and a src/lib.rs skeleton. The first WASM build failed with a missing vec! macro. In no_std, vec! comes from alloc, not the standard prelude. Claude added extern crate alloc; use alloc::vec; and rebuilt successfully.
Note: this shows a guess-then-retrieve loop. If a wrong guess compiles, the error can still slip through.
Phase 2 - Storage and Errors: Defined all storage types, eleven error variants, and six events using the sol! macro. The WASM build passed with warnings about unused fields.
This is where the error model becomes more expressive than the original. Anchor errors are essentially u32 codes with string messages:
#[error_code]
pub enum ErrorCode {
#[msg("The given owner is not part of this multisig.")]
InvalidOwner,
// ...
#[msg("Not enough owners signed this transaction.")]
NotEnoughSigners,
// ...
#[msg("The given transaction has already been executed.")]
AlreadyExecuted,
// ...
}
The Stylus version carries structured data:
sol! {
error InvalidOwner(address caller);
error NotEnoughSigners(uint64 required, uint64 provided);
error AlreadyExecuted(uint256 tx_id);
// ...
}
#[derive(SolidityError)]
pub enum ContractError {
InvalidOwner(InvalidOwner),
NotEnoughSigners(NotEnoughSigners),
AlreadyExecuted(AlreadyExecuted),
// ...
}
Clients can now programmatically read which owner was invalid and how many signatures were missing, not just that an error occurred.
Phase 3 - Core Logic: Claude implemented all seven public functions, seven view functions, and seven internal helpers. Two compile issues required API and import adjustments.
First: error[E0277]: the trait bound ‘alloy_primitives::Bytes: AbiType’ is not satisfied. The Bytes type is not ABI-encodable in the Stylus SDK. Claude switched the public API signatures from Bytes to Vec<u8>.
Second: error[E0433]: could not find ‘calls’ in ‘stylus_sdk’. The import path for the Call context had changed between SDK versions. Claude re-consulted the handbook’s external-calls chapter and found the correct path: stylus_sdk::prelude::*.
After the adjustments, the ported contract’s public interface looks like this:
#[public]
impl Multisig {
#[constructor]
pub fn constructor(&mut self, owners: Vec<Address>, threshold: U64) { // ... }
pub fn create_transaction(&mut self, target: Address, value: U256, data: Vec<u8>) -> Result<U256, ContractError> { // ... }
pub fn approve(&mut self, tx_id: U256) -> Result<(), ContractError> { // ... }
pub fn execute_transaction(&mut self, tx_id: U256) -> Result<Vec<u8>, ContractError> { // ... }
pub fn set_owners(&mut self, owners: Vec<Address>) -> Result<(), ContractError> { // ... }
pub fn change_threshold(&mut self, threshold: U64) -> Result<(), ContractError> { // ... }
pub fn set_owners_and_change_threshold(&mut self, owners: Vec<Address>, threshold: U64) -> Result<(), ContractError> { // ... }
// View functions
pub fn get_owners(&self) -> Vec<Address> { // ... }
pub fn get_threshold(&self) -> U64 { // ... }
pub fn get_transaction(&self, tx_id: U256) -> (Address, U256, Vec<u8>, Vec<bool>, bool, U32) { // ... }
pub fn is_owner(&self, addr: Address) -> bool { // ... }
pub fn get_tx_count(&self) -> U256 { // ... }
pub fn get_owner_set_seqno(&self) -> U32 { // ... }
pub fn get_approval_count(&self, tx_id: U256) -> U64 { // ... }
}
Phase 4 - Unit Tests: Twenty-three tests using the motsu harness by OpenZeppelin. They cover constructor validation, transaction creation, approval flow, execution guards, owner-management authorization, view functions, and property invariants. All pass, but what exactly was tested?
The tests break down into seven categories: constructor validation (6 tests covering valid init, single owner, empty owners, zero threshold, threshold exceeding owner count, and duplicate owners), transaction creation (4 tests: success, auto-approval by creator, ID increment, non-owner rejection), approval flow (4 tests: success, non-owner rejection, double-approval rejection, invalid transaction rejection), execution guards (2 tests: insufficient signers, invalid transaction), owner management authorization (3 tests: set_owners, change_threshold, and set_owners_and_change_threshold all rejected when called directly), view functions (2 tests: is_owner and get_transaction for a nonexistent ID), and property invariants (2 tests: threshold bounds and approval count not exceeding owner count).
This is decent coverage of the rejection paths, but it has significant gaps.
No successful execution test: execute_transaction is only tested for failure cases. There is no test that a fully-approved transaction actually executes and returns the expected result. The entire happy path of the contract’s core function is untested.
No self-governance test: The three owner-management tests only verify that direct calls are rejected. The happy path --- calling set_owners or change_threshold through execute_transaction --- is never tested. This is the contract’s most critical flow: the re-entrant self-administration pattern where the multisig governs itself by executing transactions that target its own functions. An auditor would flag this immediately.
No owner_set_seqno invalidation test: The stale-seqno rejection path, where an owner-set change invalidates pending transactions, is untested. This is a security-critical invariant carried over from the Solana implementation.
No AlreadyExecuted test: The replay-protection path, where a second execution of the same transaction is rejected, is untested.
Key migration patterns
Four patterns from this migration recur across Solana-to-Stylus ports.
Accounts become contract storage
Solana stores each piece of state in a separate, dedicated account. Stylus stores everything inside the contract.
Solana:
#[account]
pub struct Multisig {
pub owners: Vec<Pubkey>,
pub threshold: u64,
pub nonce: u8,
pub owner_set_seqno: u32,
}
#[account]
pub struct Transaction {
pub multisig: Pubkey,
pub program_id: Pubkey,
pub accounts: Vec<TransactionAccount>,
pub data: Vec<u8>,
pub signers: Vec<bool>,
pub did_execute: bool,
pub owner_set_seqno: u32,
}
Stylus:
#[storage]
pub struct Transaction {
target: StorageAddress,
value: StorageU256,
data: StorageBytes,
signers: StorageVec<StorageBool>,
did_execute: StorageBool,
owner_set_seqno: StorageU32,
}
#[storage]
#[entrypoint]
pub struct Multisig {
owners: StorageVec<StorageAddress>,
threshold: StorageU64,
owner_set_seqno: StorageU32,
tx_count: StorageU256,
transactions: StorageMap<U256, Transaction>,
}
Key shifts: Pubkey becomes Address, Vec<T> becomes StorageVec<StorageT>, and separate Transaction accounts collapse into StorageMap<U256, Transaction>. The nonce disappears (no PDA derivation in EVM), while tx_count is added as an explicit identifier counter.
PDA signers become the contract address
On Solana, the multisig executes transactions by deriving a PDA and using it to sign a cross-program invocation:
let multisig_key = ctx.accounts.multisig.key();
let seeds = &[multisig_key.as_ref(), &[ctx.accounts.multisig.nonce]];
let signer = &[&seeds[..]];
solana_program::program::invoke_signed(&ix, accounts, signer)?;On Stylus, the contract is the signer. When it makes an external call, msg.sender on the receiving end is the contract's own address:
let call_context = Call::new().value(value);
let result = self.vm().call(&call_context, target, &data);This simplifies the self-governance pattern. On Solana, set_owners requires a PDA signer account derived from specific seeds, validated through Anchor constraints. On Stylus, it is a four-line check:
fn require_self_call(&self) -> Result<(), ContractError> {
if self.vm().msg_sender() != self.vm().contract_address() {
return Err(ContractError::Unauthorized(Unauthorized {}));
}
Ok(())
}
Errors get richer
Anchor errors are essentially u32 codes with human-readable messages attached at compile time. A client learns that “not enough owners signed” but not how many did sign or how many were needed.
Stylus errors, defined through the sol! macro, carry structured data:
error NotEnoughSigners(uint64 required, uint64 provided);This improves off-chain diagnostics. Tooling can programmatically parse the error, display the gap, and help users understand exactly what went wrong.
Why structured knowledge matters
A simpler experiment is to ask an LLM to “port this to Stylus.” The result can look plausible while missing runtime-critical differences like reentrancy assumptions, storage semantics, or ABI constraints.
StylusPort improves this with retrieval plus constrained prompts: research first, implementation second, and an explicit plan with risk analysis and architecture mapping.
The two-phase workflow also creates a practical review gate: inspect the migration plan, challenge architectural choices, and correct direction before implementation.
That division of labor matches the principle we discussed before: the model accelerates boilerplate and pattern transfer, while humans validate design and audit correctness.
Conclusion
Structured knowledge plus AI execution can produce a reliable migration starting point. Here, the plan was reviewable, the generated contract compiled, and tests covered many rejection paths. What remains is the hardest work: auditing correctness, testing realistic execution paths, and validating security invariants under deployment conditions.
Your mileage may vary
Coral Multisig is a single-program, 300-line repository. Real-world Solana projects are often multi-program monorepos with shared state, internal CPIs, and dense account relationship graphs. As complexity rises, omission risk rises with it.
The practical value of this tooling is speed-to-first-working-draft, not automatic correctness. Treat generated code as a starting point that still needs disciplined review, comprehensive testing, and security audit.
For MCP server setup instructions and the latest workflow details, refer to the StylusPort repository.
The handbook is also available to read online. In addition to practical porting guidelines, it also covers the differences between Solana programs and Stylus contracts, as well as security considerations.
Get a quote for your project, schedule a call with our team, follow us on X, and sign up for our newsletter for simplified and curated Web3 security insights.


