diff --git a/articles/How to Migrate an Ethereum Protocol to Solana Contracts (Part 1)/README.md b/articles/How to Migrate an Ethereum Protocol to Solana Contracts (Part 1)/README.md index 69fb64f..67d21a6 100644 --- a/articles/How to Migrate an Ethereum Protocol to Solana Contracts (Part 1)/README.md +++ b/articles/How to Migrate an Ethereum Protocol to Solana Contracts (Part 1)/README.md @@ -16,15 +16,18 @@ intro: "A deep dive into the core mindset shift and best practices when moving c As the Solana ecosystem matures, more Ethereum (EVM) protocol teams are exploring migration to Solana to achieve higher throughput, lower transaction costs, and improved user experience. Through leading and executing multiple real-world Ethereum-to-Solana migrations, we've accumulated hands-on experience across smart contract architecture, data models, transaction design, and full-stack coordination. -This article is part of a broader series on migrating Ethereum protocols to Solana, where we break the process down into three core layers: smart contracts, backend services, and frontend interactions. If you're new to the series, we recommend starting with "[How to Migrate an Ethereum Protocol to Solana — Preamble](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-preamble?tab=engineering)," which introduces the fundamental architectural differences between the two ecosystems. +This article is part of a broader series on migrating Ethereum protocols to Solana, where we break the process down into three core layers: smart contracts, backend services, and frontend interactions. If you're new to the series, we recommend starting with [How to Migrate an Ethereum Protocol to Solana — Preamble](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-preamble?tab=engineering), which introduces the fundamental architectural differences between the two ecosystems. In this article, we focus specifically on the smart contract layer. Rather than treating migration as a simple language switch from Solidity to Rust, we examine the deeper mindset shifts required when moving from Ethereum's contract-centric model to Solana's account-centric design. Using concrete examples and real production patterns, we'll walk through the most important conceptual changes, common pitfalls, and best practices that Ethereum developers need to understand to build secure and efficient Solana programs. #### Article Navigation -- [How to Migrate an Ethereum Protocol to Solana — Preamble](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-preamble?tab=engineering): A systematic introduction to the fundamental differences between Ethereum and Solana in account models, execution mechanisms, and fee systems. -- [How to Migrate an Ethereum Protocol to Solana — Contracts (Part 1)](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-contracts-part-1?tab=engineering): A focus on the core mindset shift and best practices for contract development from Ethereum to Solana. - +- [How to Migrate an Ethereum Protocol to Solana — Preamble](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-preamble): account models, execution, and fees on both chains. +- [How to Migrate an Ethereum Protocol to Solana — Contracts (Part 1)](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-contracts-part-1): account model, CPI, PDAs, and Anchor patterns for EVM developers. +- [How to Migrate an Ethereum Protocol to Solana — Contracts (Part 2)](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-contracts-part-2): Solana limits (CU, forks, hooks, events) and a staking migration walkthrough. +- [How to Migrate an Ethereum Protocol to Solana — Frontend(Part 1)](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-frontend-part-1): wallets, RPC, and client patterns when moving a DApp frontend to Solana. +- [How to Migrate an Ethereum Protocol to Solana — Frontend(Part 2)](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-frontend-part-2): transaction building, account fetching, and event/log handling on the client. +- [How to Migrate an Ethereum Protocol to Solana — Backend](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-backend): event sync, log parsing, and state management for a production backend. --- Solana rose in popularity and matured quickly because its high performance and low costs attracted a surge of developer and user attention. Meanwhile, Ethereum (EVM) and EVM-compatible chains had massive ecosystems but faced challenges like limited scalability and high transaction fees. As a result, more Web3 developers have been turning to Solana for its improved developer and user experiences, making the migration from Ethereum to Solana a prominent trend. So, how do we effectively port the smart contracts we've mastered on Ethereum to the Solana platform? Many developers initially thought that they would only need to reprogram apps using Rust rather than Solidity, but they soon discovered that the real migration challenge rested in the fundamental differences of Solana's underlying architecture. This article aims to help experienced Ethereum developers complete a critical mental model shift so they can efficiently and securely reimplement existing contract logic the Solana way. @@ -77,11 +80,11 @@ pub mod staking { pub fn stake(ctx: Context, amount: u64) -> Result<()> { // Business logic operates on accounts passed via the context. // The context `ctx` contains all necessary accounts, - // such as `GlobalState` and `UserStakeInfo`, defined in the `Stake` struct below. - let state = &mut ctx.accounts.state; - let user_info = &mut ctx.accounts.user_stake_info; - state.total_staked += amount; - user_info.amount += amount; + // such as `PoolConfig`, `PoolState`, and `UserStakeInfo`. + let pool_state = &mut ctx.accounts.pool_state; + let user_stake_info = &mut ctx.accounts.user_stake_info; + pool_state.total_staked += amount; + user_stake_info.amount += amount; // ... Ok(()) } @@ -92,27 +95,63 @@ pub mod staking { #[derive(Accounts)] pub struct Stake<'info> { #[account(mut)] - pub state: Account<'info, GlobalState>, - #[account(mut)] - pub user_stake_info: Account<'info, UserStakeInfo>, - // ... other necessary accounts + pub user: Signer<'info>, + #[account( + seeds = [POOL_CONFIG_SEED, pool_config.pool_id.as_ref()], + bump = pool_config.bump + )] + pub pool_config: Box>, + #[account( + mut, + seeds = [POOL_STATE_SEED, pool_config.key().as_ref()], + bump = pool_state.bump, + has_one = pool_config + )] + pub pool_state: Box>, + #[account( + init_if_needed, + payer = user, + space = 8 + UserStakeInfo::INIT_SPACE, + seeds = [STAKE_SEED, pool_config.key().as_ref(), user.key().as_ref()], + bump + )] + pub user_stake_info: Box>, + pub system_program: Program<'info, System>, } // State is defined in separate account structs. #[account] -pub struct GlobalState { +#[derive(InitSpace)] +pub struct PoolConfig { + pub admin: Pubkey, + pub pool_id: Pubkey, + pub staking_mint: Pubkey, + pub reward_mint: Pubkey, + pub reward_per_second: u64, + pub bump: u8, +} + +#[account] +#[derive(InitSpace)] +pub struct PoolState { + pub pool_config: Pubkey, + pub acc_reward_per_share: u128, + pub last_reward_time: i64, pub total_staked: u64, - // ... other global state + pub total_reward_debt: i128, + pub bump: u8, } #[account] +#[derive(InitSpace)] pub struct UserStakeInfo { pub amount: u64, - // ... other user state + pub reward_debt: i128, + pub bump: u8, } ``` -Here, the `staking` program is stateless and holds no data. All data—both global `GlobalState` and per-user `UserStakeInfo`—are defined in separate `#[account]` structs. The program receives these accounts through the `Context` object (typed by the `Stake` struct), and then operates on them. +Here, the `staking` program is stateless and holds no data. All data—both pool-level `PoolConfig` / `PoolState` and per-user `UserStakeInfo`—are defined in separate `#[account]` structs. The program receives these accounts through the `Context` object (typed by the `Stake` struct), and then operates on them. This design's fundamental purpose is to enable large-scale [parallel processing](https://medium.com/solana-labs/sealevel-parallel-processing-thousands-of-smart-contracts-d814b378192). Because code and data are separated, Solana transactions will declare all accounts they will access ahead of execution and specify whether each account is read-only or writable. This allows the runtime to build a dependency graph and schedule transactions efficiently. If two transactions touch completely unrelated accounts—or both only read the same account—they can safely run in parallel. Only when one transaction needs to write to an account, other transactions that access that account (read or write) will be temporarily blocked and executed sequentially. With this fine-grained scheduling, Solana maximizes multi-core utilization to process many non-interfering transactions concurrently. This is a key element to its high throughput and low latency. @@ -147,7 +186,7 @@ pub struct Stake<'info> { #[account(mut)] pub user_token_account: Account<'info, TokenAccount>, #[account(mut)] - pub staking_vault: Account<'info, TokenAccount>, + pub staking_token: Account<'info, TokenAccount>, pub token_program: Program<'info, Token>, // ... } @@ -159,7 +198,7 @@ pub fn stake(ctx: Context, amount: u64) -> Result<()> { ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.user_token_account.to_account_info(), - to: ctx.accounts.staking_vault.to_account_info(), + to: ctx.accounts.staking_token.to_account_info(), authority: ctx.accounts.user.to_account_info(), } ), @@ -216,13 +255,16 @@ async function stakeTokens( const userStakePda = getUserStakePda(statePda, user.publicKey); // All required accounts must be explicitly passed. + const userBlacklistPda = getBlacklistPda(statePda, user.publicKey); const stakeInstruction = programClient.getStakeInstruction({ user: userSigner, - state: address(statePda.toBase58()), + poolConfig: address(statePda.toBase58()), + poolState: address(poolStatePda.toBase58()), userStakeInfo: address(userStakePda.toBase58()), userTokenAccount: address(stakingToken.toBase58()), - stakingVault: address(stakingVaultPda.toBase58()), - // ... and other accounts + stakingToken: address(stakingTokenPda.toBase58()), + tokenProgram: address(TOKEN_PROGRAM_ID.toBase58()), + blacklistEntry: address(userBlacklistPda.toBase58()), amount: amount, }); @@ -230,7 +272,7 @@ async function stakeTokens( } ``` -In this TypeScript Test, calling the `stake` instruction requires a large account object: `user` (signer), `state` (global state account), `userStakeInfo` (user staking data account), `userTokenAccount` (the user's token account), `stakingVault` (the program's vault), etc. While this makes the client call more verbose, it brings transparency and safety. Before the transaction is sent, the client code explicitly defines all accounts included in the transaction. There are no hidden contextual dependencies in a Solana transaction. +In this TypeScript Test, calling the `stake` instruction requires a large account object: `user` (signer), `poolConfig` (pool config account), `poolState` (pool runtime state account), `userStakeInfo` (user staking data account), `userTokenAccount` (the user's token account), `stakingToken` (the program's staking token account), `blacklistEntry` (the user's blacklist PDA), etc. While this makes the client call more verbose, it brings transparency and safety. Before the transaction is sent, the client code explicitly defines all accounts included in the transaction. There are no hidden contextual dependencies in a Solana transaction. Additionally, on Ethereum, upgrading a contract often requires changing client code to point to a new contract address. On Solana, you simply deploy new program code to the same program ID, achieving seamless upgrades. All business data remains untouched in their accounts because data and logic are decoupled. Since the program address doesn’t change, client code remains compatible. @@ -240,8 +282,9 @@ If you want deeper architectural context for the code patterns in this article, To put these ideas into practice, you may want to get comfortable with a different, ecosystem-specific toolchain. From language to standard libraries, Solana's ecosystem differs significantly from Ethereum's ecosystem. The table below summarizes key differences to help you build a new understanding of the differences quickly. + | **Domain** | **Ethereum Ecosystem** | **Solana Ecosystem** | **Key Notes** | -| :------------------------ | :------------------------------------ | :---------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ------------------------- | ------------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Frameworks** | Hardhat / Foundry (Solidity) | Anchor (Rust) | In the Ethereum ecosystem, Hardhat and Foundry are widely used smart contract development tools. Anchor is the de facto standard for Solana development; it uses powerful macros to greatly simplify the complexity of Solana program development. | | **Interface Standard** | ABI (Application Binary Interface) | IDL (Interface Definition Language) | Anchor automatically generates an IDL from your program code, similar to the ABI concept on Ethereum—ABI is Ethereum’s contract interaction standard, and the Solidity compiler automatically generates ABI files describing function/parameter/return binary encodings. Clients can use these IDL or ABI files to interact with your program without needing to understand the underlying implementation. | | **Standard Library** | OpenZeppelin | SPL (Solana Program Library) | OpenZeppelin is an import-and-inherit code library, whereas SPL is a set of reusable standard programs already deployed on-chain. You interact with them via Cross-Program Invocation (CPI) instead of copying code into your project. | @@ -249,6 +292,7 @@ To put these ideas into practice, you may want to get comfortable with a differe | **Network RPC** | Infura, Alchemy, QuickNode | Helius, Alchemy, QuickNode | Both ecosystems have top-tier RPC providers; only a few (like QuickNode) are multi-chain. Solana's high throughput has also led to specialized providers like Helius to offer enhanced Solana-first APIs. | | **Explorers** | Etherscan, Blockscout | Solscan, Solana Explorer, X-Ray | The Ethereum ecosystem has powerful tools like Tenderly for deep transaction simulation and debugging. In the Solana ecosystem, tools like Helius (product X-Ray) provide similar functionality. Due to Solana’s parallel transaction model, these tools focus more on visualizing value flows between accounts and CPI call chains to help developers understand complex instruction interactions. | + From this comparison, a clear pattern emerges: Ethereum development supports ideas like inheritance and extension (e.g., inheriting OpenZeppelin contracts), while Solana development supports composition and interaction (via CPI with on-chain SPL programs). We recommend that newcomers to Solana use the Anchor framework whenever possible. Unlike Ethereum's Hardhat/Foundry, which focuses on the external development flow (tests, deployment, scripting), Anchor affects how program code is written and runs. Its Macros and constraints dramatically simplify the process of writing Solana programs by handling a lot of tedious and error-prone low-level safety checks and data serialization. If you master Anchor, you'll master efficient, safe business logic on Solana. @@ -265,38 +309,102 @@ Native development requires direct interaction with Solana's low-level libraries Solana's official recommendation, meant specifically for developers migrating from Ethereum, is to choose Anchor. Anchor leverages Rust macros to simplify development, enhance safety, and ultimately automate the complex parts of native development. -Here's a simple `initialize` instruction for creating a new global state account using Anchor. Once you declare accounts and constraints, the framework handles validation and initialization for you. +Here's a simple `create_pool` instruction for creating pool config, pool state, staking token, and reward vault accounts using Anchor. Once you declare accounts and constraints, the framework handles validation and initialization for you. ```rust -// solana-staking/programs/solana-staking/src/instructions/initialize.rs -#[program] -pub mod staking { - pub fn initialize_handler(ctx: Context, reward_per_second: u64) -> Result<()> { - // Business logic is clean and focused. - let state = &mut ctx.accounts.state; - state.reward_per_second = reward_per_second; - state.admin = ctx.accounts.admin.key(); - // ... - Ok(()) - } +// solana-staking/programs/solana-staking/src/instructions/create_pool.rs +pub fn create_pool_handler( + ctx: Context, + pool_id: Pubkey, + reward_per_second: u64, +) -> Result<()> { + require!(pool_id != Pubkey::default(), StakingError::InvalidPoolId); + require!(reward_per_second > 0, StakingError::InvalidRewardPerSecond); + + let pool_config = &mut ctx.accounts.pool_config; + let pool_state = &mut ctx.accounts.pool_state; + let clock = Clock::get()?; + + pool_config.admin = ctx.accounts.admin.key(); + pool_config.pool_id = pool_id; + pool_config.staking_mint = ctx.accounts.staking_mint.key(); + pool_config.reward_mint = ctx.accounts.reward_mint.key(); + pool_config.reward_per_second = reward_per_second; + pool_config.bump = ctx.bumps.pool_config; + + pool_state.pool_config = pool_config.key(); + pool_state.acc_reward_per_share = 0; + pool_state.last_reward_time = clock.unix_timestamp; + pool_state.total_staked = 0; + pool_state.total_reward_debt = 0; + pool_state.bump = ctx.bumps.pool_state; + + Ok(()) } -// Define accounts and constraints declaratively. #[derive(Accounts)] -pub struct Initialize<'info> { +#[instruction(pool_id: Pubkey)] +pub struct CreatePool<'info> { #[account(mut)] pub admin: Signer<'info>, - // Anchor handles the creation and rent payment for this account. - #[account(init, payer = admin, space = 8 + GlobalState::INIT_SPACE)] - pub state: Account<'info, GlobalState>, + #[account( + init, + payer = admin, + space = 8 + PoolConfig::INIT_SPACE, + seeds = [POOL_CONFIG_SEED, pool_id.as_ref()], + bump + )] + pub pool_config: Box>, + #[account( + init, + payer = admin, + space = 8 + PoolState::INIT_SPACE, + seeds = [POOL_STATE_SEED, pool_config.key().as_ref()], + bump + )] + pub pool_state: Box>, + pub staking_mint: Account<'info, Mint>, + pub reward_mint: Account<'info, Mint>, + #[account( + init, + payer = admin, + token::mint = staking_mint, + token::authority = pool_config, + seeds = [STAKING_TOKEN_SEED, pool_config.key().as_ref()], + bump + )] + pub staking_token: Account<'info, TokenAccount>, + #[account( + init, + payer = admin, + token::mint = reward_mint, + token::authority = pool_config, + seeds = [REWARD_VAULT_SEED, pool_config.key().as_ref()], + bump + )] + pub reward_vault: Account<'info, TokenAccount>, pub system_program: Program<'info, System>, + pub token_program: Program<'info, Token>, } #[account] -pub struct GlobalState { +pub struct PoolConfig { pub admin: Pubkey, + pub pool_id: Pubkey, + pub staking_mint: Pubkey, + pub reward_mint: Pubkey, pub reward_per_second: u64, - // ... + pub bump: u8, +} + +#[account] +pub struct PoolState { + pub pool_config: Pubkey, + pub acc_reward_per_share: u128, + pub last_reward_time: i64, + pub total_staked: u64, + pub total_reward_debt: i128, + pub bump: u8, } ``` @@ -415,7 +523,7 @@ pub struct AccountClose<'info> { For more details, see [Mango Markets v4 source](https://github.com/blockworks-foundation/mango-v4/blob/dev/programs/mango-v4/src/accounts_ix/account_close.rs). -Our `solana-staking` example also follows this lifecycle model. The `initialize` instruction creates global state and vault accounts; the `stake` instruction uses `init` to create a user info account on first stake; and in `unstake`, if the user’s balance returns to zero, the program uses `close` to destroy their user info account and refund rent. See the repository here: [solana-staking](https://github.com/57blocks/evm-to-solana/tree/main/contract/solana-staking). +Our `solana-staking` example also follows this lifecycle model. The `create_pool` instruction creates pool config, pool state, staking token, and reward vault accounts; the `stake` instruction uses `init_if_needed` to create a user stake info account on first stake; and when a user wants to close out, the separate `close_user_stake_account` instruction destroys their user stake info account and refunds rent. See the repository here: [solana-staking](https://github.com/57blocks/evm-to-solana/tree/main/contract/solana-staking). ### Program Derived Addresses (PDA) @@ -436,7 +544,7 @@ pub struct Stake<'info> { init_if_needed, payer = user, space = 8 + UserStakeInfo::INIT_SPACE, - seeds = [STAKE_SEED, state.key().as_ref(), user.key().as_ref()], + seeds = [STAKE_SEED, pool_config.key().as_ref(), user.key().as_ref()], bump )] pub user_stake_info: Box>, @@ -447,15 +555,13 @@ pub struct Stake<'info> { #[account] #[derive(InitSpace)] pub struct UserStakeInfo { - pub owner: Pubkey, pub amount: u64, pub reward_debt: i128, - pub claimed: u64, pub bump: u8, } ``` -- `seeds = [STAKE_SEED, state.key().as_ref(), user.key().as_ref()]`: the core PDA definition. It derives `user_stake_info` from a constant `STAKE_SEED`, the global state account `state.key()`, and the user public key `user.key()`. This ensures a unique, predictable `UserStakeInfo` address per user per staking pool. +- `seeds = [STAKE_SEED, pool_config.key().as_ref(), user.key().as_ref()]`: the core PDA definition. It derives `user_stake_info` from a constant `STAKE_SEED`, the pool config account `pool_config.key()`, and the user public key `user.key()`. This ensures a unique, predictable `UserStakeInfo` address per user per staking pool. - `bump`: Anchor finds a `bump` and stores it in the PDA’s data. Future instructions use the stored `bump` to re-derive and verify the address, ensuring `user_stake_info` is legitimate, not forged. - `init_if_needed`: a convenience constraint that auto-creates this PDA on a user’s first stake. It’s feature-gated in Anchor because it can introduce reinitialization risks, so avoid it when possible. @@ -472,7 +578,7 @@ There are two reasons to do this. First, the complexity of CPI (Cross-Program In // Transfer staking tokens from user to vault let cpi_accounts = Transfer { from: ctx.accounts.user_token_account.to_account_info(), - to: ctx.accounts.staking_vault.to_account_info(), + to: ctx.accounts.staking_token.to_account_info(), authority: ctx.accounts.user.to_account_info(), }; let cpi_program = ctx.accounts.token_program.to_account_info(); @@ -523,20 +629,22 @@ Upgrades are crucial to a project’s evolution, and Ethereum and Solana offer v In early Ethereum, upgrading smart contracts was complex and risky. Because code and data are tightly coupled at one address, upgrading often meant deploying a new contract and migrating data, which can be complex and error-prone. The community developed mature Proxy patterns where data resides in a stable proxy contract and upgradeable logic contracts are referenced via pointers. Upgrades switch the logic implementation without changing the proxy address—now the de facto standard. -Solana's design is simpler and more elegant: program code and state storage are naturally separated. You can redeploy new BPF bytecode to the same program ID to upgrade the program, while state accounts (outside the program) remain intact. There is no data migration needed, significantly reducing complexity and risk. However, there's a new challenge–once an account's structure and size are set, you can’t expand it in-place. If you later add new fields to a state account that was allocated with a smaller size, you’ll get data misalignment or read errors. The recommended approach is to pre-allocate unused space (`padding`) in v1 so you can safely add fields later without changing account size: +Solana's design is simpler and more elegant: program code and state storage are naturally separated. You can redeploy new BPF bytecode to the same program ID to upgrade the program, while state accounts (outside the program) remain intact. There is no data migration needed, significantly reducing complexity and risk. However, there's a new challenge–once an account's structure and size are set, you can’t expand it in-place. If you later add new fields to a state account that was allocated with a smaller size, you’ll get data misalignment or read errors. The example below shows the current `PoolState` layout; if you expect the account to grow later, reserve extra space up front when you define the account size: ```rust -#[account(zero_copy)] -#[repr(C)] -pub struct MyState { - pub data_field_a: u64, - pub data_field_b: bool, - // Reserve 128 bytes for future upgrade - pub _reserved: [u8; 128], +#[account] +#[derive(InitSpace)] +pub struct PoolState { + pub pool_config: Pubkey, + pub acc_reward_per_share: u128, + pub last_reward_time: i64, + pub total_staked: u64, + pub total_reward_debt: i128, + pub bump: u8, } ``` -This way, when you need new fields, you can repurpose part of `_reserved` without changing the account size, keeping old accounts compatible with the new program. +This way, when you need new fields, you can reuse that preallocated space without changing the account size, keeping old accounts compatible with the new program. Also, when deploying a Solana program, you must set an upgrade authority (`upgrade authority`), which is often the deployer wallet or a multisig. This authority is the only entity that can update program bytecode. If it's compromised or removed improperly, the program could be maliciously upgraded or become immutable, so handle it with care. @@ -544,7 +652,7 @@ Also, when deploying a Solana program, you must set an upgrade authority (`upgra In Ethereum's ERC20 standard, transferring on behalf of a user usually takes two steps: the user calls `approve` to grant an allowance, and the authorized party (often a contract) then calls `transferFrom`. This exists because the account model distinguishes between the token holder and the executor, and the executor must submit a transaction separately. -In Solana’s SPL Token model, this is greatly simplified. Each token account records its _authority_ explicitly. As long as the transaction includes that authority’s signature, the program can directly call `token::transfer` to move tokens—no separate `transferFrom` needed. In other words, Solana’s runtime natively supports a **who-signs-who-authorizes** model instead of relying on contracts to check a second-layer approval. +In Solana’s SPL Token model, this is greatly simplified. Each token account records its *authority* explicitly. As long as the transaction includes that authority’s signature, the program can directly call `token::transfer` to move tokens—no separate `transferFrom` needed. In other words, Solana’s runtime natively supports a **who-signs-who-authorizes** model instead of relying on contracts to check a second-layer approval. Furthermore, Solana’s execution environment supports signature propagation across CPI: @@ -559,7 +667,7 @@ Our staking flow uses direct user signatures without proxy or PDA authority. Whe pub fn stake_handler(ctx: Context, amount: u64) -> Result<()> { let cpi_accounts = Transfer { from: ctx.accounts.user_token_account.to_account_info(), - to: ctx.accounts.staking_vault.to_account_info(), + to: ctx.accounts.staking_token.to_account_info(), authority: ctx.accounts.user.to_account_info(), }; let cpi_program = ctx.accounts.token_program.to_account_info(); @@ -570,7 +678,7 @@ pub fn stake_handler(ctx: Context, amount: u64) -> Result<()> { } ``` -Solana doesn’t need `transferFrom` because its runtime fuses _authorization_ and _execution_: if a valid signature is present in the transaction, the user has authorized the transfer without extra steps. +Solana doesn’t need `transferFrom` because its runtime fuses *authorization* and *execution*: if a valid signature is present in the transaction, the user has authorized the transfer without extra steps. ### Numerical Computation @@ -578,7 +686,7 @@ Numeric handling on Solana also requires a shift of thinking. First, regarding p When mixing multiplication and division, beware of precision loss in intermediate results. In many languages, writing `r = a / b * c` as a single expression may benefit from extended precision registers; on x86, the FPU uses 80-bit extended precision internally, only truncating to 64-bit at the end. Note that compilers may also reorder or combine operations. But if you split this into steps like `t = a / b; r = t * c;`, the intermediate result is written to memory (64-bit), then read back, causing extra precision loss. -For integer token amounts, choose `u64/u128` to avoid floating-point issues. However, for ratios, rates, and prices, floats may be necessary, and if that is the case, be careful with intermediate precision. For example, on x86, a single expression like `r = a / b * c` might compute in 80-bit precision, only truncating at the end. Note that splitting the computation into steps as described earlier (first computing t = a / b, then computing r = t \* c) forces 64-bit truncation in between, introducing additional errors. +For integer token amounts, choose `u64/u128` to avoid floating-point issues. However, for ratios, rates, and prices, floats may be necessary, and if that is the case, be careful with intermediate precision. For example, on x86, a single expression like `r = a / b * c` might compute in 80-bit precision, only truncating at the end. Note that splitting the computation into steps as described earlier (first computing t = a / b, then computing r = t c) forces 64-bit truncation in between, introducing additional errors. ## Conclusion @@ -595,3 +703,4 @@ In the next article, “From Ethereum to Solana — Contracts (Part 2),” we’ - [A Complete Guide to Solana Development for Ethereum Developers](https://solana.com/developers/evm-to-svm/complete-guide) - [Solana Development for EVM Developers](https://www.quicknode.com/guides/solana-development/getting-started/solana-development-for-evm-developers#key-architectural-differences-between-ethereum-and-solana) - [Verifying Programs](https://solana.com/docs/programs/verified-builds) + diff --git a/articles/How to Migrate an Ethereum Protocol to Solana Contracts (Part 2)/README.md b/articles/How to Migrate an Ethereum Protocol to Solana Contracts (Part 2)/README.md new file mode 100644 index 0000000..7eab5ac --- /dev/null +++ b/articles/How to Migrate an Ethereum Protocol to Solana Contracts (Part 2)/README.md @@ -0,0 +1,584 @@ +--- +published: true +title: "How to Migrate an Ethereum Protocol to Solana — Contracts (Part 2)" +author: ["Jimmy Zhao / Fullstack Engineer", "Bin Li / Tech Lead"] +createTime: 2026-03-05 +categories: ["engineering"] +subCategories: ["Blockchain & Web3"] +tags: ["Solana", "Ethereum", "Smart Contract", "Solidity", "Anchor"] +landingPages: ["Blockchain-Onchain infra"] +thumb: "./thumb.png" +thumb_h: "./thumb_h.png" +intro: "Solana contract constraints that bite during an EVM migration, plus a full staking port from Solidity to Anchor." +--- + +## Article Overview + +If you’ve already read [Contracts (Part 1)](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-contracts-part-1), you can split state across accounts and wire up Anchor instructions. Day-to-day migration still hits walls Solidity habits do not cover: mainnet-fork testing is manual and easy to get wrong; transactions have a hard compute-unit cap; CPI is one-way (no re-entrancy, no synchronous callbacks); token hooks and off-chain events are not `ERC-20` plus `emit`. Plan for these early or you refactor late. + +The first half covers those trade-offs. The second half ports staking end to end (`stake`, `unstake`, `claimRewards`) with code in [evm-to-solana](https://github.com/57blocks/evm-to-solana): implementation, tests, and deployment. + +It’s part of our series on migrating Ethereum protocols to Solana (contracts, backend, frontend). If you’re new to the topic, start with the [Preamble](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-preamble) for account models, execution, and fees. + +#### Article Navigation + +- [How to Migrate an Ethereum Protocol to Solana — Preamble](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-preamble): account models, execution, and fees on both chains. +- [How to Migrate an Ethereum Protocol to Solana — Contracts (Part 1)](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-contracts-part-1): account model, CPI, PDAs, and Anchor patterns for EVM developers. +- [How to Migrate an Ethereum Protocol to Solana — Contracts (Part 2)](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-contracts-part-2): Solana limits (CU, forks, hooks, events) and a staking migration walkthrough. +- [How to Migrate an Ethereum Protocol to Solana — Frontend(Part 1)](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-frontend-part-1): wallets, RPC, and client patterns when moving a DApp frontend to Solana. +- [How to Migrate an Ethereum Protocol to Solana — Frontend(Part 2)](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-frontend-part-2): transaction building, account fetching, and event/log handling on the client. +- [How to Migrate an Ethereum Protocol to Solana — Backend](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-backend): event sync, log parsing, and state management for a production backend. + +## Solana Limitations and Trade-offs You Should Know + +Part 1 covers how to structure programs and accounts. Below are the constraints we hit most often when porting real protocols: mainnet testing, compute budgets, CPI semantics, token hooks, and how off-chain services read state changes. + +### Testing: The Challenge of Mainnet Forking + +On Ethereum, Mainnet Forking (Hardhat, Foundry) gives you a lazily loaded local mainnet snapshot. You can call Uniswap or other live protocols without listing every account ahead of time. + +On Solana, `solana-test-validator --clone` only copies addresses you name at startup. There is no lazy full-state fork like Ethereum. To test against Jupiter you track down pools, config accounts, authorities, and anything else the instruction touches. Miss one account and the test fails in a way that is hard to debug. + +Tools like [surfpool](https://github.com/txtx/surfpool) narrow the gap: a local environment in the same spirit as Foundry’s [Anvil](https://www.alchemy.com/dapps/foundry-anvil), fetching mainnet account data on demand instead of requiring every address in a `--clone` list up front. That makes it easier to integration-test against live protocols (e.g. Jupiter) without maintaining a long clone manifest by hand. + +### Compute Unit (CU) Limits + +On Ethereum, gas limits scale with what you pay. Solana caps each transaction at **1.4 million compute units (CUs)** no matter the fee. Every instruction spends CUs, including CPIs. Exceed the budget and the whole transaction fails. + +Large loops and big in-memory passes that are routine on Ethereum often blow the CU budget on Solana. Split work across transactions or tighten the algorithm. We go deeper on limits in [Deep Dive into Resource Limitations in Solana Development — CU Edition](https://57blocks.io/blog/deep-dive-into-resource-limitations-in-solana-development-cu-edition). + +### No Callbacks / No Re-entrancy + +Re-entrancy is a familiar Solidity bug: Contract B can call back into Contract A before A finishes writing state. External calls are synchronous. A common mistake is updating balances after the call: + +```solidity +// Vulnerable Solidity Code +function withdraw() public { + uint256 userBalance = balances[msg.sender]; + require(userBalance > 0, "No balance to withdraw"); + + // The vulnerability is here: state is updated AFTER the external call. + (bool success, ) = msg.sender.call{value: userBalance}(""); + require(success, "Transfer failed"); + + // If the recipient is a malicious contract, it can re-enter this function + // before the balance is set to 0, allowing multiple withdrawals. + balances[msg.sender] = 0; +} +``` + +Solana CPI is one-way: Program A can invoke Program B, but B cannot call back into A in the same transaction. The call graph is acyclic. Even updating state after a CPI (not recommended style) avoids classic re-entrancy: + +```rust +// Solana (Anchor) equivalent logic - still safe from re-entrancy +pub fn withdraw(ctx: Context, amount: u64) -> Result<()> { + // 1. Perform checks. + let user_balance = ctx.accounts.user_vault.amount; + require!(user_balance >= amount, "Insufficient balance"); + + // 2. Interaction is performed BEFORE state change (not a best practice, but still safe). + // The Token Program is a trusted, separate program. It cannot and will not + // call back into our `withdraw` function. The execution flow is one-way. + token::transfer(cpi_context, amount)?; + + // 3. State is updated last. + ctx.accounts.user_vault.amount -= amount; + + Ok(()) +} +``` + +While `token::transfer` runs, your program waits. The Token Program will not re-enter `withdraw`, and Solana has no `fallback`/`receive` hooks for strangers to call mid-instruction. + +The trade-off: flash loans and other synchronous callback chains do not map cleanly. You usually split steps across transactions or check state in a follow-up instruction. + +### Why Hooks Are Harder + +Transfer hooks show up on both chains; the wiring differs. + +On Ethereum you usually own the token contract: override `transfer` or `_beforeTokenTransfer` for allow lists, fees, and similar rules. + +On Solana the SPL Token Program is shared and fixed. Token-2022 adds a `Transfer Hook` extension: you deploy a hook program, bind it at mint creation, and every transfer CPIs into it (read-only accounts). The hook approves or aborts the transfer. + +Setup on Solana is heavier than editing an `ERC-20`. Typical flow: + +1. Deploy a `Transfer Hook` program (allow/deny lists, fees, KYC, etc.). +2. At mint initialization, enable the Token-2022 extension and set the mint’s hook program id. +3. On each transfer, Token-2022 CPI-calls that program with the accounts you declared, often via `execute` and an `extra-account-metas` PDA so the hook sees every account it needs. A failing hook rolls back the transfer. + +Callers must use `transferChecked`; plain `transfer` fails. Many wallets and DEXs still lack hook support, so check compatibility before shipping. + +In our staking example, blacklist checks live inside `stake`, `unstake`, and `claimRewards`. That blocks protocol paths only, not arbitrary wallet-to-wallet transfers. For a global blacklist, use Transfer Hook. + +### Logs and Events + +On Ethereum, `emit` writes structured logs. Indexers subscribe by topic and indexed fields to drive UIs, analytics, and jobs. + +```solidity +// evm-staking/src/Staking.sol +// Define a structured event +event Staked(address indexed user, uint256 amount); + +function stake(uint256 amount) external { + // ... + // Emit the event with structured data + emit Staked(msg.sender, amount); +} +``` + +Solana’s built-in logging (`sol_log`, Anchor `msg!`) is printf-style text in transaction logs. Fine for debugging; poor for indexed queries. Parsers often scrape whole logs instead of filtering by type. + +Anchor’s `#[event]` wraps a struct and serializes it into logs (often Base64) so clients can parse something closer to an EVM event. + +First, define an event struct and tag it with `#[event]`: + +```rust +// solana-staking/programs/solana-staking/src/events.rs +#[event] +pub struct Staked { + pub pool: Pubkey, + pub user: Pubkey, + pub amount: u64, + pub timestamp: i64, +} +``` + +Then, use the `emit!` macro to emit this event: + +```rust +// solana-staking/programs/solana-staking/src/instructions/stake.rs +pub fn stake_handler(ctx: Context, amount: u64) -> Result<()> { + // ... (staking logic) + let pool_config = &ctx.accounts.pool_config; + let clock = Clock::get()?; + + // Emit the structured event + emit!(Staked { + pool: pool_config.pool_id, + user: ctx.accounts.user.key(), + amount, + timestamp: clock.unix_timestamp, + }); + + Ok(()) +} +``` + +`emit!` writes those bytes into the transaction log. Indexing is still weaker than Ethereum topics, but workable. Use Anchor’s `EventParser` on the client, or Helius-style webhooks and enhanced transaction APIs if you do not want to own the parser. + +For anything production-facing off-chain, use `#[event]` instead of raw `msg!`. + +## Hands-on: From Coding to Deployment + +### Case Overview + +Next: port a staking contract from Ethereum to Solana using the [evm-to-solana](https://github.com/57blocks/evm-to-solana) repo (same example as Part 1), including Foundry and Anchor code, tests, and deployment. + +**Business logic** + +- Users stake project tokens (`MyToken`) and earn `RewardToken` proportional to stake size and duration. + +**Core features** + +- `stake`: deposit `MyToken` +- `unstake`: withdraw staked `MyToken` +- `claimRewards`: claim accrued `RewardToken` + +### Ethereum Implementation (Foundry) + +On Ethereum the staking logic lives in one `Staking.sol` contract: state and functions together. + +**Contract structure and state variables** + +```solidity +// evm-staking/src/Staking.sol +contract Staking is ReentrancyGuard, Ownable { + // Token contracts to interact with + IERC20 public stakingToken; + IERC20 public rewardToken; + + // Global state + uint256 public rewardRate = 100; // 1% per day + uint256 public totalStaked; + + // Per-user state, mapping an address to their stake info + struct StakeInfo { + uint256 amount; + int256 rewardDebt; + uint256 claimed; + } + mapping(address => StakeInfo) public stakes; + + // ... events and constructor +} +``` + +All staking data (token refs, pool totals, per-user `StakeInfo`) sits in this contract’s storage. The single `mapping` is the main contrast with Solana’s per-user PDAs. + +**Core function implementations** + +`stake`, `unstake`, and `claimRewards` read and write those storage fields directly. + +```solidity +// evm-staking/src/Staking.sol +function stake(uint256 amount) external nonReentrant { + // ... (checks) + + // Pulls tokens from the user into this contract + stakingToken.transferFrom(msg.sender, address(this), amount); + + // Update user's stake info directly in the mapping + stakes[msg.sender].amount += amount; + // Update global state + totalStaked += amount; + + // ... (update timestamps and emit event) +} + +function unstake(uint256 amount) external nonReentrant { + // ... (checks and claims pending rewards) + + // Update user's stake info + stakes[msg.sender].amount -= amount; + // Update global state + totalStaked -= amount; + + // Push tokens from this contract back to the user + stakingToken.transfer(msg.sender, amount); + + // ... (emit event) +} + +function _claimRewards() private { + uint256 reward = calculateReward(msg.sender); + if (reward > 0) { + // ... (update reward debt) + // Transfer reward tokens to the user + rewardToken.transfer(msg.sender, reward); + // ... (emit event) + } +} +``` + +`stake` pulls tokens into the contract vault; `unstake` and `_claimRewards` update the mapping and push tokens out, with no CPI to an external token program. + +The full contract code is available [here](https://github.com/57blocks/evm-to-solana/tree/main/contract/evm-staking). + +### Solana Implementation (Anchor) + +The Solana version keeps the same behavior but splits code and data. + +**Program structure and account definitions** + +In Anchor, we define a stateless program containing instructions like `stake` and `unstake`, and then define all account structs used for state. + +```rust +// solana-staking/programs/solana-staking/src/lib.rs & state.rs +// The program itself is stateless. +#[program] +pub mod solana_staking { + pub fn create_pool( + ctx: Context, + pool_id: Pubkey, + reward_per_second: u64, + ) -> Result<()> { /* ... */ } + pub fn stake(ctx: Context, amount: u64) -> Result<()> { /* ... */ } + pub fn unstake(ctx: Context, amount: u64) -> Result<()> { /* ... */ } + pub fn claim_rewards(ctx: Context) -> Result<()> { /* ... */ } + // ... other instructions +} + +// Pool-level config is stored in a dedicated account (one per pool). +#[account] +pub struct PoolConfig { + pub admin: Pubkey, + pub pool_id: Pubkey, + pub staking_mint: Pubkey, + pub reward_mint: Pubkey, + pub reward_per_second: u64, + pub bump: u8, +} + +// Mutable pool runtime state is split into a separate account. +#[account] +pub struct PoolState { + pub pool_config: Pubkey, + pub acc_reward_per_share: u128, + pub last_reward_time: i64, + pub total_staked: u64, + pub total_reward_debt: i128, + pub bump: u8, +} + +// Per-user state is also in its own account, typically a PDA. +#[account] +pub struct UserStakeInfo { + pub amount: u64, + pub reward_debt: i128, + pub bump: u8, +} +``` + +`PoolConfig` and `PoolState` split fixed config from mutable pool totals; each staker gets a `UserStakeInfo` PDA (`init_if_needed` in `stake`) instead of one on-chain `mapping`. + +**Instructions and context** + +Each instruction declares every account it touches. The `stake` Context: + +```rust +// solana-staking/programs/solana-staking/src/instructions/stake.rs +#[derive(Accounts)] +pub struct Stake<'info> { + // The user performing the action (signer) + #[account(mut)] + pub user: Signer<'info>, + + #[account( + seeds = [POOL_CONFIG_SEED, pool_config.pool_id.as_ref()], + bump = pool_config.bump + )] + pub pool_config: Box>, + + #[account( + mut, + seeds = [POOL_STATE_SEED, pool_config.key().as_ref()], + bump = pool_state.bump, + has_one = pool_config + )] + pub pool_state: Box>, + + // The user's personal stake info PDA + #[account( + init_if_needed, + payer = user, + space = 8 + UserStakeInfo::INIT_SPACE, + seeds = [STAKE_SEED, pool_config.key().as_ref(), user.key().as_ref()], + bump + )] + pub user_stake_info: Box>, + + // The user's token account holding the staking tokens + #[account( + mut, + token::mint = pool_config.staking_mint, + token::authority = user + )] + pub user_token_account: Account<'info, TokenAccount>, + + // The program's vault to store the staked tokens + #[account( + mut, + seeds = [STAKING_TOKEN_SEED, pool_config.key().as_ref()], + bump + )] + pub staking_token: Account<'info, TokenAccount>, + + /// CHECK: This account may or may not exist - used for blacklist validation + #[account( + seeds = [BLACKLIST_SEED, pool_config.key().as_ref(), user.key().as_ref()], + bump, + )] + pub blacklist_entry: UncheckedAccount<'info>, + + // Required external programs + pub token_program: Program<'info, Token>, + pub system_program: Program<'info, System>, +} +``` + +`blacklist_entry` is optional: if that PDA exists with data, `stake` rejects the user. Anchor checks seeds and `has_one` links before your handler runs. + +**Core function implementation** + +`stake` CPIs the Token Program to move tokens, then writes `pool_state` and `user_stake_info` supplied in the Context. + +```rust +// solana-staking/programs/solana-staking/src/instructions/stake.rs +pub fn stake_handler(ctx: Context, amount: u64) -> Result<()> { + require!(amount > 0, StakingError::InvalidStakeAmount); + + let blacklist_info = &ctx.accounts.blacklist_entry.to_account_info(); + require!( + blacklist_info.data_is_empty() || blacklist_info.lamports() == 0, + StakingError::AddressBlacklisted + ); + + let pool_config = &ctx.accounts.pool_config; + let pool_state = &mut ctx.accounts.pool_state; + let user_stake = &mut ctx.accounts.user_stake_info; + let clock = Clock::get()?; + + update_pool(pool_config, pool_state, &clock)?; + + // 1. Command the Token Program to transfer tokens via CPI + let cpi_accounts = Transfer { + from: ctx.accounts.user_token_account.to_account_info(), + to: ctx.accounts.staking_token.to_account_info(), + authority: ctx.accounts.user.to_account_info(), + }; + let cpi_program = ctx.accounts.token_program.to_account_info(); + let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); + token::transfer(cpi_ctx, amount)?; + + // 2. Update the data on the user_stake_info account + user_stake.amount += amount; + let debt_delta = calculate_share_value(amount, pool_state.acc_reward_per_share)?; + user_stake.reward_debt += debt_delta; + user_stake.bump = ctx.bumps.user_stake_info; + + // 3. Update the data on the pool state account + pool_state.total_staked += amount; + pool_state.total_reward_debt += debt_delta; + + emit!(Staked { + pool: pool_config.pool_id, + user: ctx.accounts.user.key(), + amount, + timestamp: clock.unix_timestamp, + }); + + Ok(()) +} +``` + +Custody is a CPI to the Token Program; balances and reward debt are written to the `pool_state` and `user_stake_info` accounts passed into the instruction. + +The full Solana implementation is available [here](https://github.com/57blocks/evm-to-solana/tree/main/contract/solana-staking). + +### Contract Testing + +Foundry and Anchor take different approaches to tests. + +**Framework comparison** + +- **Ethereum / Foundry:** tests in Solidity against the contract, with `vm.prank` and other cheatcodes. +- **Solana / Anchor:** tests in TypeScript against a local validator, closer to how a client calls your program, with more setup boilerplate. + +**Foundry test example** + +From `evm-staking`, a direct `stake` test: + +```solidity +// evm-staking/test/Staking.t.sol +function testStake() public { + uint256 stakeAmount = 1000 * 10 ** 18; + + // Simulate the call coming from user1 + vm.startPrank(user1); + // User must first approve the staking contract + myToken.approve(address(staking), stakeAmount); + // Call the stake function + staking.stake(stakeAmount); + vm.stopPrank(); + + // Assertions are made directly against the contract's state + (uint256 stakedAmount, ,) = staking.getStakeInfo(user1); + assertEq(stakedAmount, stakeAmount); + assertEq(staking.totalStaked(), stakeAmount); +} +``` + +**Anchor test example** + +On Solana, the test script does more setup work: creating mock users, token accounts, then building and sending a full transaction to call the `stake` instruction. + +```typescript +// solana-staking/tests/solana-staking.test.ts +describe("Stake", () => { + it("should allow user to stake tokens", async () => { + // 1. Setup: Create a test user and their token accounts + const { user, userSigner } = await createTestUser(svm); + const { stakingToken, rewardToken } = await setupUserWithTokens( + provider, + admin, + user, + stakingMint, + rewardMint + ); + const stakeAmount = toToken(100); + + // 2. Action: Build and send the transaction to call the 'stake' instruction + await stakeTokens(user, userSigner, stakingToken, rewardToken, stakeAmount); + + // 3. Assertion: verify staking token account balance and account state + const stakingTokenAccount = getAccount(provider, stakingTokenPda); + expect(Number(stakingTokenAccount.amount)).to.equal(Number(stakeAmount)); + + const userStakePda = getUserStakePda(statePda, user.publicKey); + const userStakeInfo = getUserStakeInfo(provider, userStakePda); + expect(userStakeInfo).to.not.be.null; + expect(userStakeInfo!.amount.toString()).to.equal(stakeAmount.toString()); + expect(userStakeInfo!.rewardDebt.toString()).to.equal("0"); + + const globalState = getGlobalState(provider, statePda); + expect(globalState!.totalStaked.toString()).to.equal( + stakeAmount.toString() + ); + }); +}); +``` + +Foundry tests usually exercise contract internals directly; Anchor tests call the program through the client, which is closer to end-to-end integration testing. + +### Contract Deployment + +Deployment differs at the protocol level and in the tooling. + +**Ethereum / Foundry deployment** + +On Ethereum, deploying a contract is essentially sending a special transaction: the `to` field is empty, and the `data` field contains the compiled bytecode. Once miners include it in a block, the EVM executes the constructor logic, creates a new contract account, and stores the code at that address. + +We deploy with a Foundry script in this repo. + +```bash +# Run the deployment script using forge +forge script script/Deploy.s.sol --rpc-url --broadcast --verify +``` + +This command runs `Deploy.s.sol`, deploys the `Staking` contract to the specified network, and uses `--verify` to automatically upload source code to Etherscan for verification. The full deployment script is available [here](https://github.com/57blocks/evm-to-solana/blob/main/contract/evm-staking/script/Deploy.s.sol). + +**Solana / Anchor deployment** + +Deploying a Solana program uploads the compiled binary (BPF, usually a `.so` in `target/deploy/`) to a program account. Business state stays in separate accounts. Anchor wraps the CLI steps: + +```bash +# First, build the program to get the BPF bytecode +anchor build + +# Then, run the deploy command for the initial deployment +anchor deploy --provider.cluster +``` + +Set the cluster with `--provider.cluster` (`localnet`, `devnet`, or `mainnet-beta`). To ship new logic without migrating state, rebuild and upgrade the same program id: + +```bash +# After making changes, build the new version +anchor build + +# Then, use the upgrade command +anchor upgrade target/deploy/your_program_name.so --provider.cluster +``` + +For more detailed steps and caveats, see our project’s [deployment doc](https://github.com/57blocks/evm-to-solana/blob/main/contract/solana-staking/DEPLOYMENT.md). + +Each `forge script` deploy on Ethereum usually gets a new contract address and empty storage. `anchor deploy` / `anchor upgrade` keeps the program id; only the executable changes while pool and user accounts stay put. + +## Summary + +[Part 1](https://57blocks.com/blog/how-to-migrate-an-ethereum-protocol-to-solana-contracts-part-1) covered stateless programs, explicit accounts, CPI, and PDAs. Here we added practical limits: mainnet fork testing, CU caps, one-way CPI, Transfer Hook, and Anchor events for indexing. + +The staking example uses the same three instructions on both chains with different layout: one Solidity contract vs. split pool/user accounts and Token Program CPIs. + +Before you port, decide what is global state vs. per-user PDAs, which steps call SPL programs, and whether any instruction needs splitting for CU. Treat it as a redesign, not a line-by-line port. + +Up next in the series: frontend and backend changes after the contracts move. + +## References + +- [Moving from Ethereum Development to Solana](https://solana.com/news/evm-to-svm) +- [EVM vs. SVM: Smart Contracts](https://solana.com/developers/evm-to-svm/smart-contracts) +- [How to Migrate From Ethereum to Solana: A Guide for Devs](https://www.helius.dev/blog/how-to-migrate-from-ethereum-to-solana) +- [Basic Knowledge Needed for Migrating from EVM to Solana](https://medium.com/@easypass.inc/basic-knowledge-needed-for-migrating-from-evm-to-solana-7814b29c8bd5) +- [A Complete Guide to Solana Development for Ethereum Developers](https://solana.com/developers/evm-to-svm/complete-guide) +- [Solana Development for EVM Developers](https://www.quicknode.com/guides/solana-development/getting-started/solana-development-for-evm-developers#key-architectural-differences-between-ethereum-and-solana) +- [Verifying Programs](https://solana.com/docs/programs/verified-builds) diff --git a/articles/How to Migrate an Ethereum Protocol to Solana Contracts (Part 2)/thumb.png b/articles/How to Migrate an Ethereum Protocol to Solana Contracts (Part 2)/thumb.png new file mode 100644 index 0000000..cee4a45 Binary files /dev/null and b/articles/How to Migrate an Ethereum Protocol to Solana Contracts (Part 2)/thumb.png differ diff --git a/articles/How to Migrate an Ethereum Protocol to Solana Contracts (Part 2)/thumb_h.png b/articles/How to Migrate an Ethereum Protocol to Solana Contracts (Part 2)/thumb_h.png new file mode 100644 index 0000000..99beff9 Binary files /dev/null and b/articles/How to Migrate an Ethereum Protocol to Solana Contracts (Part 2)/thumb_h.png differ