Token transfers
Ethereum uses approve + transferFrom to let a contract pull tokens. Solana has no separate token contract per token - your program calls SPL Token via CPI, and the user's signature on the transaction is the authorization.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
IERC20 is an interface to another contract on chain. Each token is its own deployment, each with its own balance ledger and its own transferFrom semantics.
/// The contract holds tokens by being the `to` of a `transferFrom`. The user
/// must `approve(this, amount)` first; the contract then pulls.
contract Treasury {
IERC20 public immutable token;
constructor(IERC20 _token) {
token = _token;
}
function deposit(uint256 amount) external {
token.transferFrom(msg.sender, address(this), amount);
}
The user must call approve(this, amount) first - that writes allowance[user][this] on the token contract. Then transferFrom(user, this, amount) reads that allowance, decrements it, and moves the balance. Two transactions, two state writes, one persistent allowance to manage.
}
// No `approve` + `transferFrom` two-step. The user's signature on the
// transaction IS the authorization; the program issues a CPI to SPL Token
// to move tokens from the user's Token Account into the vault Token Account.
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer};
declare_id!("11111111111111111111111111111111");
anchor_spl::token imports bindings to the SPL Token program - a single on-chain program that every fungible token reuses. The token itself is just configuration on this program; nobody deploys their own ERC-20 equivalent.
#[program]
pub mod treasury {
use super::*;
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
let cpi = CpiContext::new(
ctx.accounts.token_program.key(),
Transfer {
from: ctx.accounts.user_ata.to_account_info(),
to: ctx.accounts.vault.to_account_info(),
authority: ctx.accounts.user.to_account_info(),
},
);
token::transfer(cpi, amount)?;
Ok(())
}
CpiContext::new(token_program.key(), Transfer { ... }) builds a cross-program invocation. There's no upfront approve - the user signs the deposit transaction, and the authority field on the CPI says "this signer is allowed to move tokens out of from". SPL Token verifies the signature is present and moves the balance. One transaction, zero persistent state.
Anchor 1.0.2 detail:
CpiContext::newtakes the program'sPubkey(token_program.key()), not itsAccountInfo. This is the most common 1.0 breaking-change footgun for code ported from older examples.
}
#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(mut)]
pub user_ata: Account<'info, TokenAccount>,
#[account(mut)]
pub vault: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
}
The user's Token Account, the vault Token Account, and the SPL Token program itself all show up as typed fields on Deposit<'info>. The client passes them in - the program does not "look up" balances or derive accounts on the fly.