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

Account lifecycle

Ethereum storage slots can't be deleted - you zero them and gate on a sentinel. Solana accounts come and go: init allocates them, close = X deletes them and refunds the rent.

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

/// Solidity storage slots can't be deleted. The user opens a position, then
/// `close` zeroes the entry - but `positions[user]` still exists, just with
/// a zero `amount`. Every future operation has to gate on `amount > 0`.
contract Deposits {
    mapping(address => uint256) public positions;

    function open(uint256 amount) external {
        require(positions[msg.sender] == 0, "already open");
        positions[msg.sender] = amount;
        // ... pull tokens ...
    }
"Open" means writing a non-zero sentinel.

The contract has no real "create" step. open checks that the existing slot is zero (meaning "not yet opened"), then writes a non-zero amount. The slot is the live/dead flag and the data, both in one field.


    function close() external {
        uint256 amount = positions[msg.sender];
        require(amount > 0, "no position");
        positions[msg.sender] = 0;
        // ... refund tokens ...
    }
}
"Close" means writing zero. The slot still exists.

Solidity storage slots can't be deleted. Clearing the entry just sets positions[msg.sender] = 0 - the slot remains, holding zero. Every later operation has to gate on amount > 0 to know whether the position is actually live. Get the sentinel wrong and you have a replay or double-spend bug.


port ↓
Anchor 1.0.2programs/deposits/src/lib.rsSOLANA
// On Solana, accounts come and go. `open` creates the position PDA with
// `init`; `close_position` closes it with `close = user`, refunding the
// rent. After close, the PDA doesn't exist - a third call to `open` would
// succeed (start a new position) and a third call to `close_position`
// would fail at account validation. No `amount > 0` sentinel needed.

use anchor_lang::prelude::*;

declare_id!("11111111111111111111111111111111");

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

    pub fn open(ctx: Context<Open>, amount: u64) -> Result<()> {
        ctx.accounts.position.amount = amount;
        // ... pull tokens ...
        Ok(())
    }
`open` allocates the account.

The handler just writes the amount; account creation happens at validation time via init on the constraint (see below). A second open for the same user fails before the handler runs because the PDA already exists - no require(positions == 0) needed in the body.


    pub fn close_position(_ctx: Context<ClosePosition>) -> Result<()> {
        // ... refund tokens ...
        // Account closure happens automatically via `close = user`.
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Open<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
        init,
        payer = user,
        space = 8 + 8,
        seeds = [b"position", user.key().as_ref()],
        bump,
    )]
    pub position: Account<'info, Position>,
    pub system_program: Program<'info, System>,
}
`init` actually creates the account.

init allocates the position PDA at the start of the instruction. If the account already exists, validation rejects the call - the "is this slot already open?" check is structural, not arithmetic. The supporter pays the refundable rent (~0.001 SOL per KB) for the account to live on-chain.


#[derive(Accounts)]
pub struct ClosePosition<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
        mut,
        seeds = [b"position", user.key().as_ref()],
        bump,
        close = user, // <- deletes the account, refunds rent to `user`
    )]
    pub position: Account<'info, Position>,
}
`close = user` actually deletes it.

close = user tears the account down at the end of a successful instruction and refunds its rent to user. After close, the PDA doesn't exist - a third call to close_position fails at validation, not at a runtime amount > 0 check. Replay protection becomes a property of the account graph rather than a flag you have to maintain. (Caveat: close = X only fires on a successful instruction - if the handler errors, the account stays live.)


#[account]
pub struct Position {
    pub amount: u64,
}