From 69c2b47abf33753755929b76edab9fc120e167bd Mon Sep 17 00:00:00 2001 From: Raad05 Date: Wed, 8 Apr 2026 16:24:50 +0600 Subject: [PATCH 1/3] doc: solidity vs rust syntax comparision --- guides/cheatsheets/solidity-vs-rust.md | 614 +++++++++++++++++++++++++ 1 file changed, 614 insertions(+) create mode 100644 guides/cheatsheets/solidity-vs-rust.md diff --git a/guides/cheatsheets/solidity-vs-rust.md b/guides/cheatsheets/solidity-vs-rust.md new file mode 100644 index 0000000..6830353 --- /dev/null +++ b/guides/cheatsheets/solidity-vs-rust.md @@ -0,0 +1,614 @@ +# Solidity vs Rust: Syntax Comparison + +This guide compares smart contract syntax and patterns between Solidity and Rust. + +--- + +## Table of Contents + +1. [Types](#types) +2. [Functions](#functions) +3. [Error Handling](#error-handling) +4. [Events & Logs](#events--logs) +5. [Storage Patterns](#storage-patterns) +6. [Common Operations](#common-operations) +7. [Deploy & Test Commands](#deploy--test-commands) + +--- + +## Types + +### Basic Types + +| Operation | Solidity | Rust | +| -------------------- | ------------------------------- | ------------------------------------------------ | +| **Unsigned Integer** | `uint256 x = 10;` | `let x: u64 = 10;` | +| **Signed Integer** | `int256 x = -10;` | `let x: i64 = -10;` | +| **Boolean** | `bool isActive = true;` | `let is_active: bool = true;` | +| **String** | `string memory text = "hello";` | `let text: String = "hello".to_string();` | +| **Address** | `address wallet = 0x123...;` | `let pubkey: Pubkey = Pubkey::from_str("...")?;` | +| **Bytes** | `bytes32 hash;` | `let hash: [u8; 32] = [0; 32];` | + +### Complex Types + +**Solidity - Structs:** + +```solidity +struct User { + address wallet; + uint256 balance; + bool active; +} +``` + +**Rust - Structs:** + +```rust +#[derive(Debug, Clone)] +pub struct User { + pub wallet: Pubkey, + pub balance: u64, + pub active: bool, +} +``` + +**Solidity - Arrays & Mappings:** + +```solidity +uint256[] public balances; +mapping(address => uint256) public userBalances; +mapping(address => mapping(address => uint256)) public allowances; +``` + +**Rust - Vectors & HashMaps:** + +```rust +let mut balances: Vec = Vec::new(); +let mut user_balances: HashMap = HashMap::new(); +let mut allowances: HashMap> = HashMap::new(); +``` + +--- + +## Functions + +### Function Declaration + +**Solidity:** + +```solidity +// Public function +function transfer(address to, uint256 amount) public returns (bool) { + require(balances[msg.sender] >= amount, "Insufficient balance"); + balances[msg.sender] -= amount; + balances[to] += amount; + return true; +} + +// View function (read-only) +function getBalance(address user) public view returns (uint256) { + return balances[user]; +} + +// Internal function +function _validateAmount(uint256 amount) internal pure { + require(amount > 0, "Amount must be positive"); +} +``` + +**Rust (Solana Program):** + +```rust +// Instruction handler +pub fn transfer( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let from = &accounts[0]; + let to = &accounts[1]; + + require!(from.is_signer, "From account must sign"); + + // Transfer logic + let mut from_balance = from.try_borrow_mut_data()?; + let mut to_balance = to.try_borrow_mut_data()?; + + // Perform transfer + Ok(()) +} + +// Helper function +fn validate_amount(amount: u64) -> Result<(), ProgramError> { + if amount == 0 { + return Err(ProgramError::InvalidArgument); + } + Ok(()) +} +``` + +### Function Visibility + +| Visibility | Solidity | Rust | +| ------------ | --------------------------------- | ---------------------------------- | +| **Public** | `public` - callable from anywhere | Functions exported from module | +| **Internal** | `internal` - only within contract | Functions marked with `pub(crate)` | +| **Private** | `private` - only in the contract | Functions without `pub` | +| **External** | `external` - only from outside | N/A (use public) | + +--- + +## Error Handling + +### Solidity Error Handling + +```solidity +// Using require +require(amount > 0, "Amount must be positive"); + +// Using assert +assert(balance >= amount); + +// Using revert +if (amount > balance) { + revert("Insufficient balance"); +} + +// Custom errors (Solidity 0.8.4+) +error InsufficientBalance(uint256 available, uint256 required); + +function withdraw(uint256 amount) public { + if (amount > balance) { + revert InsufficientBalance(balance, amount); + } +} +``` + +### Rust Error Handling + +```rust +// Using Result type +fn withdraw(amount: u64) -> Result<(), ProgramError> { + if amount > balance { + return Err(ProgramError::InsufficientFunds); + } + Ok(()) +} + +// Using custom errors +#[derive(Debug)] +pub enum TokenError { + InsufficientBalance, + InvalidAmount, + Unauthorized, +} + +// Using the ? operator +fn transfer(amount: u64) -> Result<(), TokenError> { + validate_amount(amount)?; // Early return on error + execute_transfer(amount)?; + Ok(()) +} + +// Using macros (Anchor framework) +pub fn transfer(ctx: Context, amount: u64) -> Result<()> { + require!(amount > 0, "Amount must be positive"); + require!( + ctx.accounts.from.balance >= amount, + "Insufficient balance" + ); + Ok(()) +} +``` + +--- + +## Events & Logs + +### Solidity Events + +```solidity +// Event declaration +event Transfer(address indexed from, address indexed to, uint256 amount); +event Approval(address indexed owner, address indexed spender, uint256 amount); + +// Emitting events +function transfer(address to, uint256 amount) public { + balances[msg.sender] -= amount; + balances[to] += amount; + emit Transfer(msg.sender, to, amount); +} + +function approve(address spender, uint256 amount) public { + allowances[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); +} +``` + +### Rust Logging (Solana) + +```rust +// Using msg! macro +msg!("Transfer initiated from {:?} to {:?}", from, to); + +// Emitting events with anchor +#[event] +pub struct TransferEvent { + pub from: Pubkey, + pub to: Pubkey, + pub amount: u64, +} + +// In instruction handler +emit!(TransferEvent { + from: ctx.accounts.from.key(), + to: ctx.accounts.to.key(), + amount: 100, +}); + +// Or using direct logging +msg!("Transfer: from={}, to={}, amount={}", from, to, 100); +``` + +--- + +## Storage Patterns + +### Solidity State Variables + +```solidity +// Contract storage +contract Token { + mapping(address => uint256) public balances; + mapping(address => mapping(address => uint256)) public allowances; + uint256 public totalSupply; + address public owner; + + constructor() { + owner = msg.sender; + totalSupply = 1000000 * 10**18; + balances[msg.sender] = totalSupply; + } +} +``` + +### Rust Account Data (Solana) + +```rust +// Using Anchor framework +use anchor_lang::prelude::*; + +#[account] +pub struct TokenAccount { + pub mint: Pubkey, + pub owner: Pubkey, + pub balance: u64, + pub delegate: Option, + pub delegated_amount: u64, + pub is_frozen: bool, +} + +// Initializing account +pub fn initialize_account( + ctx: Context, +) -> Result<()> { + let account = &mut ctx.accounts.token_account; + account.mint = ctx.accounts.mint.key(); + account.owner = ctx.accounts.owner.key(); + account.balance = 0; + Ok(()) +} + +// Custom PDA (Program Derived Address) +#[account] +pub struct Vault { + pub authority: Pubkey, + pub bump: u8, +} + +// Creating PDA +let (vault_pda, bump) = Pubkey::find_program_address( + &[b"vault", authority.as_ref()], + &program_id, +); +``` + +### Storage Cost Comparison + +| Aspect | Solidity | Rust (Solana) | +| -------------------- | ------------------------------- | ---------------------------------- | +| **Cost Model** | Gas per operation | Rent (storage) + Compute | +| **Storage Payment** | One-time deployment | Annual rent (recovered on closure) | +| **Data Persistence** | Permanent (contract owner pays) | Rent-based (account owner pays) | +| **Mutable State** | Implicit in contract | Must pass as mutable account | + +--- + +## Common Operations + +### Token Transfer + +**Solidity (ERC-20):** + +```solidity +function transfer(address to, uint256 amount) public returns (bool) { + require(balances[msg.sender] >= amount, "Insufficient balance"); + + balances[msg.sender] -= amount; + balances[to] += amount; + + emit Transfer(msg.sender, to, amount); + return true; +} +``` + +**Rust (SPL Token):** + +```rust +pub fn transfer( + ctx: Context, + amount: u64, +) -> Result<()> { + require!(amount > 0, "Amount must be greater than zero"); + + let mut from_account = ctx.accounts.from_token_account.load_mut()?; + let mut to_account = ctx.accounts.to_token_account.load_mut()?; + + require!(from_account.amount >= amount, "Insufficient balance"); + + from_account.amount -= amount; + to_account.amount += amount; + + emit!(TransferEvent { + from: ctx.accounts.from_token_account.key(), + to: ctx.accounts.to_token_account.key(), + amount, + }); + + Ok(()) +} +``` + +### Check Balance + +**Solidity:** + +```solidity +function balanceOf(address account) public view returns (uint256) { + return balances[account]; +} + +// Usage +uint256 myBalance = token.balanceOf(msg.sender); +``` + +**Rust:** + +```rust +pub fn get_balance(ctx: Context) -> Result { + let account = &ctx.accounts.token_account.load()?; + Ok(account.amount) +} + +// Usage +let balance = get_balance(&client, token_account)?; +``` + +### Approval & TransferFrom + +**Solidity:** + +```solidity +function approve(address spender, uint256 amount) public returns (bool) { + allowances[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; +} + +function transferFrom( + address from, + address to, + uint256 amount +) public returns (bool) { + require(balances[from] >= amount, "Insufficient balance"); + require(allowances[from][msg.sender] >= amount, "Allowance exceeded"); + + balances[from] -= amount; + balances[to] += amount; + allowances[from][msg.sender] -= amount; + + emit Transfer(from, to, amount); + return true; +} +``` + +**Rust:** + +```rust +pub fn approve( + ctx: Context, + amount: u64, +) -> Result<()> { + let delegate = &mut ctx.accounts.delegate; + delegate.delegated_amount = amount; + + emit!(ApprovalEvent { + owner: ctx.accounts.owner.key(), + spender: ctx.accounts.spender.key(), + amount, + }); + + Ok(()) +} + +pub fn transfer_from( + ctx: Context, + amount: u64, +) -> Result<()> { + require!( + ctx.accounts.delegate.delegated_amount >= amount, + "Allowance exceeded" + ); + + // Perform transfer + ctx.accounts.delegate.delegated_amount -= amount; + + Ok(()) +} +``` + +--- + +## Deploy & Test Commands + +### Solidity (Hardhat) + +**1. Setup:** + +```bash +npm install --save-dev hardhat +npx hardhat init +npm install @openzeppelin/contracts +``` + +**2. Compile:** + +```bash +npx hardhat compile +``` + +**3. Deploy:** + +```bash +npx hardhat run scripts/deploy.js --network localhost +``` + +**4. Test:** + +```bash +npx hardhat test +npx hardhat test test/Token.test.js --grep "transfer" +``` + +**5. Deploy to Testnet:** + +```bash +npx hardhat run scripts/deploy.js --network sepolia +``` + +**Example Test:** + +```javascript +describe("Token", function () { + it("Should transfer tokens", async function () { + const { token, addr1, addr2 } = await loadFixture(deployToken); + + await token.transfer(addr1.address, 100); + expect(await token.balanceOf(addr1.address)).to.equal(100); + }); +}); +``` + +### Rust (Anchor on Solana) + +**1. Setup:** + +```bash +cargo install anchor-cli +anchor init my_program +cd my_program +``` + +**2. Build:** + +```bash +anchor build +``` + +**3. Deploy (Local):** + +```bash +solana config set --url localhost +solana-test-validator +anchor deploy +``` + +**4. Test:** + +```bash +anchor test +anchor test --skip-local-validator +``` + +**5. Deploy to Devnet:** + +```bash +solana config set --url devnet +anchor deploy +``` + +**Example Test:** + +```rust +#[cfg(test)] +mod tests { + use super::*; + use anchor_lang::prelude::*; + + #[tokio::test] + async fn test_transfer() { + let program = anchor_lang::prelude::Program::new( + Id, + anchor_client::Client::new( + anchor_client::Cluster::Localnet, + Rc::new(anchor_client::Wallet::new( + std::fs::read( + &shellexpand::tilde("~/.config/solana/id.json") + )?, + )?), + anchor_client::RequestBuilder::default(), + ), + ); + + // Test logic here + } +} +``` + +### Key Deployment Differences + +| Aspect | Solidity | Rust (Solana) | +| ----------------------- | ----------------------- | ----------------------------- | +| **Compilation** | `solc` compiler | `cargo build` | +| **Bytecode Format** | EVM opcodes | SBPF (eBPF) | +| **Storage** | Contract state | Account data | +| **Upgrades** | Proxy pattern | CRATES (Anchor) or ASDROP | +| **Testing Environment** | Hardhat, Truffle | Anchor, solana-test-validator | +| **Local Testing** | Hardhat node | solana-test-validator | +| **Network Tokens** | Testnet ETH from faucet | Devnet SOL from airdrop | + +--- + +## Quick Reference Table + +| Task | Solidity | Rust | +| --------------------- | -------------------------- | ------------------------------ | +| **Check balance** | `balances[addr]` | `account.load()?.amount` | +| **Transfer** | `balances[from] -= amount` | Modify account state | +| **Emit event** | `emit Transfer(...)` | `emit!(Event {...})` | +| **Require condition** | `require(cond, msg)` | `require!(cond, err_msg)` | +| **Get caller** | `msg.sender` | `ctx.accounts.signer.key()` | +| **Access time** | `block.timestamp` | `Clock::get()?.unix_timestamp` | +| **Revert** | `revert("msg")` | `return Err(Error)` | +| **Storage** | State variables | Account data | + +--- + +## Resources + +- [Solidity Documentation](https://docs.soliditylang.org/) +- [Rust Documentation](https://doc.rust-lang.org/) +- [Anchor Framework](https://www.anchor-lang.com/) +- [Solana Documentation](https://docs.solana.com/) +- [ERC-20 Standard](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/) +- [SPL Token Documentation](https://spl.solana.com/token) From 704ed25aa8523863f72620c488f94480fb38acc4 Mon Sep 17 00:00:00 2001 From: Raad05 Date: Thu, 9 Apr 2026 19:39:04 +0600 Subject: [PATCH 2/3] doc: update solidity vs rust cheat sheet --- guides/cheatsheets/solidity-vs-rust.md | 206 ++++++++++++++----------- 1 file changed, 120 insertions(+), 86 deletions(-) diff --git a/guides/cheatsheets/solidity-vs-rust.md b/guides/cheatsheets/solidity-vs-rust.md index 6830353..f09a229 100644 --- a/guides/cheatsheets/solidity-vs-rust.md +++ b/guides/cheatsheets/solidity-vs-rust.md @@ -2,6 +2,8 @@ This guide compares smart contract syntax and patterns between Solidity and Rust. +Rust examples in this guide target Anchor on Solana unless explicitly labeled as Native Solana. + --- ## Table of Contents @@ -60,12 +62,33 @@ mapping(address => uint256) public userBalances; mapping(address => mapping(address => uint256)) public allowances; ``` -**Rust - Vectors & HashMaps:** +**Rust - Vectors (in-memory) & PDA Accounts (mapping equivalent):** ```rust +// In-memory collection inside a single instruction (not persistent state) let mut balances: Vec = Vec::new(); -let mut user_balances: HashMap = HashMap::new(); -let mut allowances: HashMap> = HashMap::new(); + +// Mapping-like persistent state on Solana uses PDA-derived accounts. +#[account] +pub struct UserBalance { + pub owner: Pubkey, + pub balance: u64, +} + +#[derive(Accounts)] +pub struct UpsertUserBalance<'info> { + #[account( + init_if_needed, + payer = signer, + space = 8 + 32 + 8, + seeds = [b"user-balance", signer.key().as_ref()], + bump + )] + pub user_balance: Account<'info, UserBalance>, + #[account(mut)] + pub signer: Signer<'info>, + pub system_program: Program<'info, System>, +} ``` --- @@ -96,35 +119,49 @@ function _validateAmount(uint256 amount) internal pure { } ``` -**Rust (Solana Program):** +**Rust (Anchor on Solana):** ```rust -// Instruction handler -pub fn transfer( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - let from = &accounts[0]; - let to = &accounts[1]; +pub fn transfer_sol(ctx: Context, amount: u64) -> Result<()> { + require!(amount > 0, CustomError::InvalidAmount); - require!(from.is_signer, "From account must sign"); + let from = &ctx.accounts.from; + let to = &ctx.accounts.to; - // Transfer logic - let mut from_balance = from.try_borrow_mut_data()?; - let mut to_balance = to.try_borrow_mut_data()?; + require!( + **from.to_account_info().lamports.borrow() >= amount, + CustomError::InsufficientBalance + ); + + **from.to_account_info().try_borrow_mut_lamports()? -= amount; + **to.to_account_info().try_borrow_mut_lamports()? += amount; - // Perform transfer Ok(()) } +#[derive(Accounts)] +pub struct TransferSol<'info> { + #[account(mut)] + pub from: Signer<'info>, + #[account(mut)] + pub to: SystemAccount<'info>, +} + // Helper function -fn validate_amount(amount: u64) -> Result<(), ProgramError> { +fn validate_amount(amount: u64) -> Result<()> { if amount == 0 { - return Err(ProgramError::InvalidArgument); + return err!(CustomError::InvalidAmount); } Ok(()) } + +#[error_code] +pub enum CustomError { + #[msg("Amount must be positive")] + InvalidAmount, + #[msg("Insufficient balance")] + InsufficientBalance, +} ``` ### Function Visibility @@ -164,39 +201,33 @@ function withdraw(uint256 amount) public { } ``` -### Rust Error Handling +### Rust Error Handling (Anchor) ```rust -// Using Result type -fn withdraw(amount: u64) -> Result<(), ProgramError> { - if amount > balance { - return Err(ProgramError::InsufficientFunds); - } - Ok(()) -} - // Using custom errors -#[derive(Debug)] +#[error_code] pub enum TokenError { + #[msg("Insufficient balance")] InsufficientBalance, + #[msg("Amount must be positive")] InvalidAmount, + #[msg("Unauthorized")] Unauthorized, } -// Using the ? operator -fn transfer(amount: u64) -> Result<(), TokenError> { - validate_amount(amount)?; // Early return on error - execute_transfer(amount)?; +fn validate_amount(amount: u64) -> Result<()> { + require!(amount > 0, TokenError::InvalidAmount); Ok(()) } -// Using macros (Anchor framework) -pub fn transfer(ctx: Context, amount: u64) -> Result<()> { - require!(amount > 0, "Amount must be positive"); - require!( - ctx.accounts.from.balance >= amount, - "Insufficient balance" - ); +// Using require! and the ? operator +pub fn withdraw(ctx: Context, amount: u64) -> Result<()> { + validate_amount(amount)?; // Early return on error + + let vault = &mut ctx.accounts.vault; + require!(vault.balance >= amount, TokenError::InsufficientBalance); + + vault.balance -= amount; Ok(()) } ``` @@ -225,13 +256,13 @@ function approve(address spender, uint256 amount) public { } ``` -### Rust Logging (Solana) +### Rust Logging (Anchor and Native Solana) ```rust -// Using msg! macro +// Native Solana logging msg!("Transfer initiated from {:?} to {:?}", from, to); -// Emitting events with anchor +// Anchor events #[event] pub struct TransferEvent { pub from: Pubkey, @@ -315,12 +346,12 @@ let (vault_pda, bump) = Pubkey::find_program_address( ### Storage Cost Comparison -| Aspect | Solidity | Rust (Solana) | -| -------------------- | ------------------------------- | ---------------------------------- | -| **Cost Model** | Gas per operation | Rent (storage) + Compute | -| **Storage Payment** | One-time deployment | Annual rent (recovered on closure) | -| **Data Persistence** | Permanent (contract owner pays) | Rent-based (account owner pays) | -| **Mutable State** | Implicit in contract | Must pass as mutable account | +| Aspect | Solidity | Rust (Solana) | +| -------------------- | ---------------------------------- | ---------------------------------------------------------- | +| **Cost Model** | Gas per operation | Compute + account storage allocation | +| **Storage Payment** | One-time deployment | One-time rent-exempt deposit (refundable on account close) | +| **Data Persistence** | Permanent (contract owner pays) | Persistent while account stays allocated | +| **Mutable State** | Implicit in contract storage slots | Must pass writable accounts | --- @@ -547,45 +578,45 @@ solana config set --url devnet anchor deploy ``` -**Example Test:** +**Example Test (TypeScript/Mocha, standard Anchor flow):** -```rust -#[cfg(test)] -mod tests { - use super::*; - use anchor_lang::prelude::*; - - #[tokio::test] - async fn test_transfer() { - let program = anchor_lang::prelude::Program::new( - Id, - anchor_client::Client::new( - anchor_client::Cluster::Localnet, - Rc::new(anchor_client::Wallet::new( - std::fs::read( - &shellexpand::tilde("~/.config/solana/id.json") - )?, - )?), - anchor_client::RequestBuilder::default(), - ), - ); - - // Test logic here - } -} +```typescript +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { expect } from "chai"; +import { MyProgram } from "../target/types/my_program"; + +describe("my_program", () => { + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + const program = anchor.workspace.MyProgram as Program; + + it("executes transfer instruction", async () => { + const tx = await program.methods + .transfer(new anchor.BN(100)) + .accounts({ + from: provider.wallet.publicKey, + to: provider.wallet.publicKey, + }) + .rpc(); + + expect(tx).to.be.a("string"); + }); +}); ``` ### Key Deployment Differences -| Aspect | Solidity | Rust (Solana) | -| ----------------------- | ----------------------- | ----------------------------- | -| **Compilation** | `solc` compiler | `cargo build` | -| **Bytecode Format** | EVM opcodes | SBPF (eBPF) | -| **Storage** | Contract state | Account data | -| **Upgrades** | Proxy pattern | CRATES (Anchor) or ASDROP | -| **Testing Environment** | Hardhat, Truffle | Anchor, solana-test-validator | -| **Local Testing** | Hardhat node | solana-test-validator | -| **Network Tokens** | Testnet ETH from faucet | Devnet SOL from airdrop | +| Aspect | Solidity | Rust (Solana) | +| ----------------------- | ----------------------- | ------------------------------------------------------------------- | +| **Compilation** | `solc` compiler | `cargo build` | +| **Bytecode Format** | EVM opcodes | SBF (eBPF-derived) | +| **Storage** | Contract state | Account data | +| **Upgrades** | Proxy pattern | Native upgrade authority (BPF Loader Upgradeable), `anchor upgrade` | +| **Testing Environment** | Hardhat, Truffle | Anchor, solana-test-validator, Surfpool | +| **Local Testing** | Hardhat node | solana-test-validator, Surfpool | +| **Network Tokens** | Testnet ETH from faucet | Devnet SOL from airdrop | --- @@ -608,7 +639,10 @@ mod tests { - [Solidity Documentation](https://docs.soliditylang.org/) - [Rust Documentation](https://doc.rust-lang.org/) -- [Anchor Framework](https://www.anchor-lang.com/) -- [Solana Documentation](https://docs.solana.com/) +- [Anchor Errors (require!, custom errors)](https://www.anchor-lang.com/docs/features/errors) +- [Anchor Account Constraints](https://www.anchor-lang.com/docs/references/account-constraints) +- [Solana Accounts](https://solana.com/docs/core/accounts) +- [Solana Program Deployment and Upgrades](https://solana.com/docs/programs/deploying) +- [Solana Documentation](https://solana.com/docs) - [ERC-20 Standard](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/) -- [SPL Token Documentation](https://spl.solana.com/token) +- [SPL Token Program](https://www.solana-program.com/docs/token) From cbf1b1ba1d10505243f7fb909bf1eb9906e3cbd7 Mon Sep 17 00:00:00 2001 From: Petar Stoev Date: Thu, 9 Apr 2026 16:58:43 +0300 Subject: [PATCH 3/3] fix: correct SPL token examples to use CPI pattern - Replace manual account mutation with token::transfer and token::approve CPIs via anchor_spl - Fix require! to use error codes instead of string literals - Fix load()/load_mut() to direct field access on Account types - Update quick reference table accordingly --- guides/cheatsheets/solidity-vs-rust.md | 98 ++++++++++++++++---------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/guides/cheatsheets/solidity-vs-rust.md b/guides/cheatsheets/solidity-vs-rust.md index f09a229..1f67de2 100644 --- a/guides/cheatsheets/solidity-vs-rust.md +++ b/guides/cheatsheets/solidity-vs-rust.md @@ -373,31 +373,47 @@ function transfer(address to, uint256 amount) public returns (bool) { } ``` -**Rust (SPL Token):** +**Rust (SPL Token via CPI):** ```rust -pub fn transfer( - ctx: Context, +use anchor_spl::token::{self, Token, TokenAccount, Transfer}; + +pub fn transfer_tokens( + ctx: Context, amount: u64, ) -> Result<()> { - require!(amount > 0, "Amount must be greater than zero"); - - let mut from_account = ctx.accounts.from_token_account.load_mut()?; - let mut to_account = ctx.accounts.to_token_account.load_mut()?; - - require!(from_account.amount >= amount, "Insufficient balance"); + require!(amount > 0, TokenError::InvalidAmount); - from_account.amount -= amount; - to_account.amount += amount; + token::transfer( + CpiContext::new( + ctx.accounts.token_program.to_account_info(), + Transfer { + from: ctx.accounts.from.to_account_info(), + to: ctx.accounts.to.to_account_info(), + authority: ctx.accounts.authority.to_account_info(), + }, + ), + amount, + )?; emit!(TransferEvent { - from: ctx.accounts.from_token_account.key(), - to: ctx.accounts.to_token_account.key(), + from: ctx.accounts.from.key(), + to: ctx.accounts.to.key(), amount, }); Ok(()) } + +#[derive(Accounts)] +pub struct TransferTokens<'info> { + #[account(mut)] + pub from: Account<'info, TokenAccount>, + #[account(mut)] + pub to: Account<'info, TokenAccount>, + pub authority: Signer<'info>, + pub token_program: Program<'info, Token>, +} ``` ### Check Balance @@ -417,12 +433,8 @@ uint256 myBalance = token.balanceOf(msg.sender); ```rust pub fn get_balance(ctx: Context) -> Result { - let account = &ctx.accounts.token_account.load()?; - Ok(account.amount) + Ok(ctx.accounts.token_account.amount) } - -// Usage -let balance = get_balance(&client, token_account)?; ``` ### Approval & TransferFrom @@ -453,36 +465,46 @@ function transferFrom( } ``` -**Rust:** +**Rust (SPL Token via CPI):** ```rust -pub fn approve( - ctx: Context, +use anchor_spl::token::{self, Approve, Token, TokenAccount, Transfer}; + +pub fn approve_delegate( + ctx: Context, amount: u64, ) -> Result<()> { - let delegate = &mut ctx.accounts.delegate; - delegate.delegated_amount = amount; - - emit!(ApprovalEvent { - owner: ctx.accounts.owner.key(), - spender: ctx.accounts.spender.key(), + token::approve( + CpiContext::new( + ctx.accounts.token_program.to_account_info(), + Approve { + to: ctx.accounts.token_account.to_account_info(), + delegate: ctx.accounts.delegate.to_account_info(), + authority: ctx.accounts.owner.to_account_info(), + }, + ), amount, - }); + )?; Ok(()) } -pub fn transfer_from( - ctx: Context, +pub fn delegated_transfer( + ctx: Context, amount: u64, ) -> Result<()> { - require!( - ctx.accounts.delegate.delegated_amount >= amount, - "Allowance exceeded" - ); - - // Perform transfer - ctx.accounts.delegate.delegated_amount -= amount; + // The delegate (spender) is the signer, transferring from the owner's account + token::transfer( + CpiContext::new( + ctx.accounts.token_program.to_account_info(), + Transfer { + from: ctx.accounts.from.to_account_info(), + to: ctx.accounts.to.to_account_info(), + authority: ctx.accounts.delegate.to_account_info(), + }, + ), + amount, + )?; Ok(()) } @@ -624,7 +646,7 @@ describe("my_program", () => { | Task | Solidity | Rust | | --------------------- | -------------------------- | ------------------------------ | -| **Check balance** | `balances[addr]` | `account.load()?.amount` | +| **Check balance** | `balances[addr]` | `account.amount` | | **Transfer** | `balances[from] -= amount` | Modify account state | | **Emit event** | `emit Transfer(...)` | `emit!(Event {...})` | | **Require condition** | `require(cond, msg)` | `require!(cond, err_msg)` |