Create your own
Lesson illustration

Rust/Anchor vs. Solidity/Solang: Accounts, Serialization, Clients, Testing, and Security

Welcome to the Solidity-on-Solana module. You already know that Solana applications revolve around explicitly supplied accounts rather than a contract quietly owning its whole state. This lesson sharpens that idea by comparing two ways to write Solana programs:

  • Rust with Anchor, Solana’s dominant framework-oriented workflow.
  • Solidity with Solang, a compiler that targets Solana’s SBF runtime rather than the EVM.

The syntax is not the decisive difference. The important question is where each tool places responsibility: declaring accounts, encoding state and instructions, generating clients, building tests, and preventing unauthorized state changes. By the end, you should be able to read a small feature in either style and identify what the framework/compiler handles and what the application must still enforce.


One runtime, two authoring models

Both Anchor and Solang produce programs that execute on the Solana Virtual Machine. A transaction supplies a program instruction plus an ordered list of accounts. The runtime enforces whether an account was included as writable and whether a private key actually signed the transaction. Your program then decides whether those accounts are the right accounts for the requested operation.

The core structural fact is:

  • A Solana program account holds executable code.
  • Separate data accounts hold mutable state.
  • The program can modify only the accounts it owns, subject to runtime rules and the account privileges supplied to the instruction.

Anchor makes this model explicit through Rust types and account-context structs. Solang lets you retain Solidity contract syntax, but adapts it to the same explicit-account runtime. A Solang contract therefore does not behave like an Ethereum contract deployed to one address that contains both bytecode and storage.

Solana — Solang Solidity Compiler v0.3.5-65-g2a05dd4 documentation

Read the Solang documentation’s Solana target overview. It establishes the account model that prevents Solidity syntax from being mistaken for EVM semantics.

In “Solana Overview,” read the account split. Notice that a deployed program binary can serve multiple contract-state accounts. Then, in “Solana Account Management,” read from constructor and function account declarations. Focus on what the annotations declare: payer, read-only or mutable accounts, and signers. Finally, in “msg.sender not available on Solana,” read the authorization explanation and example. The key lesson is that signer status and application authority are separate checks.

A useful comparison is a simple counter whose stored authority may increment its value. Its business rule is straightforward: only the stored authority may change the counter. How the two ecosystems express the surrounding account protocol differs substantially.


Account declarations: typed contexts versus annotations

Anchor: the instruction context is a Rust type

In Anchor, every instruction has a Context<T>, where T is a struct marked with #[derive(Accounts)]. Its fields describe every account required by the instruction. Account wrapper types and constraints provide declarative validation before the instruction body runs.

A simplified Anchor counter increment might look like this:

#[account]
pub struct Counter {
    pub authority: Pubkey,
    pub value: u64,
}

#[derive(Accounts)]
pub struct Increment<'info> {
    #[account(mut, has_one = authority)]
    pub counter: Account<'info, Counter>,

    pub authority: Signer<'info>,
}

pub fn increment(ctx: Context<Increment>) -> Result<()> {
    let counter = &mut ctx.accounts.counter;

    counter.value = counter
        .value
        .checked_add(1)
        .ok_or(error!(CounterError::Overflow))?;

    Ok(())
}

This declaration carries several claims:

  1. counter is writable.
  2. It must deserialize as the Counter account type and be owned by this program.
  3. The authority field stored in counter must equal the public key supplied as authority.
  4. That authority account must have signed.
  5. The increment must not overflow.

The first four conditions are largely represented in the account context. The handler can focus on the state transition itself.

Solang: function annotations declare transaction accounts

A Solang contract still has Solidity fields and functions, but it must declare the external Solana accounts an instruction needs. Those accounts become available through tx.accounts.

Conceptually, the same feature is written like this:

import "solana";

contract Counter {
    address authority;
    uint64 value;

    constructor(address initialAuthority) {
        authority = initialAuthority;
    }

    @signer(authorityAccount)
    function increment() external {
        assert(tx.accounts.authorityAccount.key == authority);
        assert(tx.accounts.authorityAccount.is_signer);

        value += 1;
    }

    function get() public view returns (uint64) {
        return value;
    }
}

The crucial line is not the increment. It is the pair of assertions:

  • authorityAccount.key == authority establishes authorization: this is the key recorded in the contract’s state.
  • authorityAccount.is_signer establishes authentication: that key authorized this transaction.

The @signer(authorityAccount) annotation requests a signer account in the Solana instruction interface. It does not mean “the caller is automatically trusted.” This is the practical consequence of Solana having no universal msg.sender: a transaction may have several signers, and a CPI may involve several meaningful accounts.

For contract initialization, Solang can annotate a constructor with items such as @payer, @seed, @bump, and @space. These play a role similar to an Anchor initialization context containing init, payer, seeds, bump, and space. In either approach, account creation is an explicit protocol, not an incidental side effect of declaring a state variable.

What changes for the developer?

ConcernRust / AnchorSolidity / Solang
Required accountsFields of #[derive(Accounts)] contextFunction and constructor annotations
Read/write privilegeTypes and #[account(mut)] constraints@account or @mutableAccount annotations
Signer requirementSigner<'info>@signer or @mutableSigner annotation
State/account relationshipConstraints such as has_one, seeds, and addressExplicit checks against tx.accounts and persistent contract state
Access in business logicctx.accounts.countertx.accounts.authorityAccount and contract storage

Anchor’s account context acts rather like a strongly typed boundary object between a TypeScript caller and Rust business logic. This is especially valuable in a larger codebase: the required capabilities of an instruction are visible in one compact struct. Solang preserves a more familiar contract-oriented surface, but the account interface remains visible through annotations and tx.accounts.


Serialization: typed account data versus compiler-managed contract storage

Solana stores account data as bytes. Every framework must answer two questions:

  1. How are instruction arguments encoded into bytes?
  2. How are persistent account bytes interpreted as program state?

Anchor serialization

An Anchor #[account] struct becomes a serializable account type. When you request:

pub counter: Account<'info, Counter>

Anchor validates and deserializes the supplied account before your handler uses it. When the handler completes successfully, modified account data is serialized back.

Anchor also places a type identifier, normally called an account discriminator, at the beginning of an account’s data. This makes it harder to accidentally deserialize one Anchor account type as another. Account sizing must reserve space for it. Thus, a state allocation commonly takes the form:

The leading is the conventional discriminator allocation; it is not storage for one of your own fields.

Program Structure

Read Anchor’s program-structure documentation to see how an account context combines interface declaration, validation, and serialization.

In “#[derive(Accounts)] macro” and its “Account Validation” subsection, read the account-context explanation, then continue through the validation discussion. Relate each context field to a transaction account that a client must supply. Next read “Account Discriminator,” from the discriminator explanation. Pay attention to the two moments at which the discriminator matters: initialization and deserialization.

Solang serialization

Solang compiles Solidity state variables into a layout stored in the contract’s Solana data account. You work with fields such as authority and value as contract storage, while the compiler manages their byte-level representation.

That convenience has a boundary: Solang’s state layout is not Ethereum storage. Nor should you assume that a Solang data account is interchangeable with an arbitrary Anchor Account<T> merely because both target Solana. Let the relevant compiler output, IDL, and generated client encode and decode data; do not invent a byte layout in a frontend.

For instruction dispatch, Solang’s Solana target uses 8-byte function discriminators. This differs from the EVM convention of a 4-byte function selector. The larger lesson is broader: Solidity source can look familiar while the wire protocol, account list, and execution environment are Solana-specific.

Solana also makes sizing visible. When a Solang constructor creates the state account on-chain, @space(...) must allocate enough storage. In Anchor, you calculate account space in the init constraint. Dynamic data therefore needs capacity planning in both ecosystems: a String, vector, mapping-like design, or future state expansion cannot be treated as unbounded storage.


Client generation and the TypeScript boundary

For a frontend developer, this is often the operationally important difference. The on-chain program is only one half of the application; the client must create the exact instruction bytes and account metadata the runtime expects.

Anchor’s integrated path

Anchor generates an IDL describing program instructions, their arguments, accounts, and custom types. Its TypeScript client uses that IDL to provide an instruction-building API. In normal use, you name the instruction, supply typed arguments, identify accounts by meaningful field names, and attach any needed signer keypairs.

That is a considerable reduction in hand-written glue, but it does not relieve the client of its protocol role. The client must still derive the correct PDA, choose the user’s wallet public key, supply the correct token program or system program, and mark signers correctly.

Solang’s Anchor-compatible path

Solang can produce an Anchor-compatible IDL, enabling an Anchor JavaScript/TypeScript client to invoke a Solang-compiled program. This is useful because it permits one client stack across an application even if an individual program is authored in Solidity.

There are compatibility details worth treating as part of the interface contract:

  • Solidity function names are exposed in camelCase to the Anchor JavaScript client. A Solidity set_new_authority call becomes setNewAuthority().
  • Numeric parameters are represented as Anchor BN values rather than ordinary JavaScript number or native bigint.
  • The client can decode return values only for Solidity functions marked view or pure.
  • Named Solidity return values become camelCase; unnamed ones receive positional names such as return0.

These points make the two paths similar at the call site but not identical. In particular, a state-changing Solang function should communicate its result through account state, emitted data where supported, or subsequent account reads—not through an EVM-style expectation that a client receives decoded return data from the submitted transaction.

A disciplined frontend boundary looks like this in either stack:

  1. Derive or fetch the state account address.
  2. Build an instruction using generated metadata rather than manually concatenating bytes.
  3. Supply every required account with correct writable and signer status.
  4. Ask the wallet to sign and send.
  5. Confirm the transaction.
  6. Refetch or subscribe to the state account for the authoritative result.

The difference is that Anchor’s Rust account context and IDL are designed together, whereas Solang maps Solidity declarations and annotations into an Anchor-compatible interface.


Testing: similar network tests, different failure surfaces

Both approaches need integration tests against a local Solana validator. A test must prove more than “this function returns the expected value.” It must prove that the client assembled the correct transaction and that the on-chain program rejects incorrect account combinations.

With Anchor, the conventional workflow is tightly integrated:

  • build the Rust program,
  • deploy it to the local validator used by the test environment,
  • execute TypeScript tests through the generated Anchor client,
  • inspect account state after each instruction.

This makes it natural to test the account constraints directly. For the counter, useful cases include a valid authority increment, a non-signer authority account, a different signer, and a wrongly typed or wrongly derived state account.

With Solang, the test still uses a local validator and can use an Anchor TypeScript client, but the build boundary changes:

  • compile Solidity for Solana SBF,
  • deploy the resulting program and create or initialize its data account,
  • use the generated compatible interface to invoke functions,
  • assert persistent state and failure behavior.

The tests should be nearly identical in intent, but diagnosis differs. An Anchor failure may identify a failed account constraint before handler logic executes. A Solang test may expose a missing annotation, incorrect supplied account list, failed Solidity assertion, unsupported language feature, or a mismatch in the generated client interface.

For both, write negative tests deliberately. Solana programs often fail not because the arithmetic was wrong, but because a client supplied an account that was writable but unauthorized, signed but unrelated, or structurally valid but from the wrong protocol.


Security responsibility: automation is not authorization

The comparison can be summarized as a division of labor.

Security questionAnchor assistanceSolang assistanceYour responsibility
Did the account sign?Signer type and constraintsSigner annotation exposes signer statusRequire it where the action needs consent
Is this a valid account type owned by this program?Account<T> validates expected structure and ownershipContract storage is compiler-managedValidate external accounts and avoid unsafe assumptions
Is this signer the stored authority?has_one = authority can express itExplicit comparison with tx.accounts is typicalDesign and enforce the authorization rule
Is this the intended PDA?seeds and bump constraints can validate itConstructor seed and bump annotations can create itUse a canonical seed scheme and verify intended relationships
Is the account writable when it must be?mut constraintMutable-account annotationRequest only necessary privileges; test failures
Are arithmetic and business invariants safe?No framework can infer themNo compiler annotation can infer themUse checked arithmetic and validate state transitions
Is an external program the intended one?Program/account constraints can helpAccounts are passed explicitlyValidate program identities and CPI assumptions

Anchor is often described as “safer,” but that phrase needs precision. It makes many structural checks concise, visible, and difficult to omit accidentally. It cannot determine who should be allowed to withdraw funds, whether a price is stale, whether a PDA seed scheme is sound, or whether an integer operation matches the protocol’s economic rules.

Solang does not restore EVM security assumptions. In particular:

  • msg.sender is unavailable.
  • payable and msg.value do not provide EVM-style value-transfer semantics.
  • external calls must receive the accounts they need for a Solana CPI.
  • wide EVM-style integers can consume more Solana compute than -bit values.

For Solana-oriented code, uint64 is commonly the appropriate representation for amounts and counters because Solana balances and machine registers are -bit oriented. Use larger widths only when the domain genuinely requires them, and account for the compute cost.

A practical review habit is to read every instruction in two passes:

  1. Capability pass: Which accounts can mutate, sign, fund, or execute another program?
  2. Relationship pass: What proves each supplied account is the particular user, authority, PDA, mint, token account, or external program the business rule expects?

Anchor helps encode both passes declaratively. Solang requires more of the second pass to be explicit in contract logic. Neither permits skipping it.


Choosing the right mental model

Do not frame the choice as “Rust is Solana-native and Solidity is familiar.” The useful framing is:

  • Choose Anchor when you want the most integrated Solana-native account model, mature conventions for account constraints and tests, and direct access to the Rust ecosystem.
  • Consider Solang when Solidity source compatibility provides a concrete advantage, while accepting that the target is still Solana: explicit accounts, signer-based authorization, compute constraints, and non-EVM client behavior remain fundamental.

In both cases, the durable skill is the same: specify an instruction’s account capabilities and validate the relationships among those accounts before mutating state.


You have now compared the two stacks across their most consequential boundaries:

  • Account declarations: Anchor uses typed Accounts contexts; Solang uses account annotations and tx.accounts.
  • Serialization: Anchor serializes typed account structs with discriminators; Solang manages Solidity storage in an explicit Solana data account.
  • Clients: Anchor’s IDL drives its TypeScript client; Solang can expose an Anchor-compatible interface with naming, numeric, and return-value caveats.
  • Testing: both need local-validator integration tests, especially negative account-validation cases.
  • Security: signer status is not authorization, and framework support never replaces protocol-specific checks.

Next, you will assess when Solang is actually suitable for a project by examining its Solidity compatibility boundaries, maintenance considerations, and tooling maturity.

Can't find a good explanation? Sign up and we'll make it for you

Sign up