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.
// 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;
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;
}
}
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.
// `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(())
}
}
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>,
}
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,
}
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.