ETH TO SOL
$npx skills add solana-foundation/eth-to-sol-skill

PDAs

Ethereum's mapping(K => V) is one storage tree inside the contract. Solana has no mapping - every entry is its own on-chain account at a deterministic address derived from the key.

SolidityScoreboard.solETHEREUM
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// One contract, one storage tree. Every user's score lives at a slot
/// derived from `(scores.slot, msg.sender)` - Solidity does the lookup
/// implicitly when you write `scores[msg.sender]`.
contract Scoreboard {
    mapping(address => uint64) public scores;
A mapping is an implicit storage tree.

mapping(address => uint64) scores lets every address have a slot inside the contract. scores[alice] lands at keccak256(alice, scores.slot). The contract resolves the slot on demand, reads from its own storage, and that's it - always-there, free, implicit.


    function bump() external {
        scores[msg.sender] += 1;
    }
}
Lookup is invisible at the call site.

scores[msg.sender] += 1 looks like one operation. There is no "create the entry first" step - the slot is conceptually always present, zero-valued until written.


port ↓
Anchor 1.0.2programs/scoreboard/src/lib.rsSOLANA
// `mapping(address => uint64)` becomes one PDA per key. The Solidity slot
// `scores[msg.sender]` translates to an account derived from
// `[b"score", user.key()]` - each entry is its own on-chain account.

use anchor_lang::prelude::*;

declare_id!("11111111111111111111111111111111");

#[program]
pub mod scoreboard {
    use super::*;

    pub fn bump(ctx: Context<Bump>) -> Result<()> {
        ctx.accounts.score.value = ctx
            .accounts
            .score
            .value
            .checked_add(1)
            .ok_or(ProgramError::ArithmeticOverflow)?;
        Ok(())
    }
}
The handler is one mutation - no lookup, no scan.

ctx.accounts.score is the supporter's PDA, already loaded (or created) by Anchor before this body runs. The handler just increments. No iter().find(), no "is this user new?" branch - account validation did that work.


#[derive(Accounts)]
pub struct Bump<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    // `init_if_needed` is the closest analog to Solidity's implicit
    // `mapping[key] = ...` - the row is created on first bump, no-op'd on
    // subsequent ones. The supporter pays the (refundable) rent.
    #[account(
        init_if_needed,
        payer = user,
        space = 8 + 8,
        seeds = [b"score", user.key().as_ref()],
        bump,
    )]
    pub score: Account<'info, Score>,
    pub system_program: Program<'info, System>,
}
One account per key, addressed by seeds.

mapping(address => uint64) becomes one account per supporter - a PDA whose address is derived from [b"score", user.key()]. Each entry is its own on-chain account with its own owner, its own rent, and its own write-lock. init_if_needed is the closest analog to Solidity's "set the value" idiom: it creates the account on first call and is a no-op on later ones. The supporter pays the (refundable) rent for their row.


#[account]
pub struct Score {
    pub value: u64,
}
Per-key accounts unlock parallelism.

The Solana runtime locks every writable account a transaction touches. Per-key PDAs mean two unrelated supporters bumping their scores at the same time run in parallel - their accounts are disjoint, so there's nothing to serialize on.