摘要:在 Remix VM 写带 mapping(address=>uint256) 与 Updated/Transfer 事件的 Ledger 合约:0.8.28 编译、VM 部署(构造 logs:1),再用本机 eth-tester + web3.py 核对 balanceOf 与事件;不需测试网 ETH,不碰主网。
上一课在 Remix 里跑通了最小合约;这一课只盯两样东西:mapping(address => uint256) 怎么存余额,以及写入时发出的 event 怎么在终端 / 回执里核对。演示全程用 Remix IDE 的 Remix VM 部署,再用本机 eth-tester + web3.py 8.0.0 等价跑一遍;不需要测试网 ETH,也绝不碰主网 / 真私钥。
合约结构入门见 Solidity 第一课:Remix VM 部署;回执里 Logs 字段含义见 以太坊交易回执怎么读;用 JS/Python 读链上状态可接着看 ethers.js 读合约 与 web3.py 读 ERC-20。
一、mapping 与 event:分别解决什么
mapping 是合约存储里的键值表:本例用地址当键、用 uint256 当余额。它不能像数组一样 length 遍历,所以链上「有没有改成功」通常靠两件事核对——调用只读 balanceOf,以及看交易回执里的事件日志。
| 构件 | 本例 | 核对时看什么 |
|---|---|---|
mapping(address => uint256) |
_balances(private) |
只能通过 balanceOf / 自己写的 getter 读 |
event Updated |
who indexed + old/new |
每次改余额打一条;构造函数也会打 |
event Transfer |
from/to indexed + amount | transfer 时额外打,方便按地址筛 |
| 写入函数 | setBalance / transfer |
改存储必须发交易;VM 里是假 Gas |
| 只读函数 | balanceOf / owner |
eth_call,不改状态、不产生 logs |
二、合约代码:先写再部署
在 Remix Playground 新建或粘贴下面的 Ledger.sol(本场实拍文件名仍显示为工作区里的 HelloWorld.sol,合约名是 Ledger,以编译器下拉为准):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title Ledger — mapping + event demo for Remix VM / eth-tester
contract Ledger {
mapping(address => uint256) private _balances;
address public owner;
event Updated(address indexed who, uint256 oldBal, uint256 newBal);
event Transfer(address indexed from, address indexed to, uint256 amount);
constructor() {
owner = msg.sender;
_balances[msg.sender] = 1000;
emit Updated(msg.sender, 0, 1000);
}
function balanceOf(address account) public view returns (uint256) {
return _balances[account];
}
function setBalance(address account, uint256 newBal) public {
require(msg.sender == owner, "only owner");
uint256 old = _balances[account];
_balances[account] = newBal;
emit Updated(account, old, newBal);
}
function transfer(address to, uint256 amount) public returns (bool) {
require(_balances[msg.sender] >= amount, "insufficient");
uint256 fromOld = _balances[msg.sender];
uint256 toOld = _balances[to];
_balances[msg.sender] = fromOld - amount;
_balances[to] = toOld + amount;
emit Transfer(msg.sender, to, amount);
emit Updated(msg.sender, fromOld, fromOld - amount);
emit Updated(to, toOld, toOld + amount);
return true;
}
}

三、编译:0.8.28 对齐 pragma
左侧「Solidity 编译器」选 0.8.28(满足 ^0.8.20),编译成功后左侧图标绿勾,合约列表出现 Ledger (…sol):

四、Remix VM 部署:构造函数就会打 Updated
「部署 & 发交易」环境选 Remix VM (Osaka),合约选 Ledger,点部署。本场实例地址形如 0xd91…39138;终端绿色勾,构造交易 logs: 1(正是 constructor 里那条 Updated(owner, 0, 1000)):

接着在已部署实例里:用 setBalance(某地址, 新余额) 写入,再点蓝色 balanceOf 读回;或调用 transfer 看终端是否出现更多 logs。回执字段怎么读,对照 交易回执 Logs 即可——Remix 终端里的 decoded logs 与浏览器 Logs 标签是同一类信息。
存储改了,外部索引器 / 前端通常靠日志重建「谁的余额变了」;只靠轮询
balanceOf 既慢也容易漏历史。Updated.who、Transfer.from/to 进 topics,便于按地址过滤;非 indexed 的数值进 data。只在 Remix VM / 本地 eth-tester。不要切 Injected Provider 连主网,也不要粘贴私钥。
五、本机 eth-tester:把 mapping 读值与事件打印出来
同一份 solc 0.8.28 字节码,用 web3.py 8.0.0 + eth-tester 本地假链再跑:部署 → setBalance(acct2, 42) → transfer(acct2, 100)。终端应看到构造 Updated(…,0,1000)、写入后 balanceOf(acct2)=42,再 transfer 后 owner 900 / acct2 142,且 transfer 回执 logs: 3(1 条 Transfer + 2 条 Updated):

solc --bin --abi --optimize -o build Ledger.sol --overwrite
# 再用 web3.py + EthereumTesterProvider 加载 ABI/bytecode 部署并 call
六、本机 solc 再核 ABI(可选)
确认 ABI 里确实有 Transfer / Updated 与 balanceOf / setBalance / transfer,字节码约 1129 字节:

常见坑
- 以为 mapping 能 for 循环扫全表:不能;要自己维护索引数组,或靠事件重建。
- 写了 storage 却忘了 emit:链上能
call到新值,但浏览器 / 索引器很难追溯历史变更。 - 用 float 心算余额: Solidity 用整数;换算小数位是另一层(见 ERC-20 decimals 文)。
- 环境选成 Injected Provider:可能弹钱包、误连主网。第一课/第二课都停在 Remix VM。
- 把 Remix VM 地址当测试网地址去区块浏览器搜:VM 交易只在当前会话有效,刷新即失效。
相关阅读
- Solidity 第一课:合约结构与 Remix 本地/VM 部署怎么跑通
- 以太坊交易回执怎么读:Status、Logs 与 Cumulative Gas 字段核对
- ethers.js 读合约:ABI、address 与 call 字段
- web3.py 读 ERC-20:balanceOf、decimals 换算与裸 eth_call 核对
说明:配图为 2026-10-09 在 app.remix.live(中文)、本机 solc 0.8.28 与 eth-tester 实拍/实跑。Remix VM 与 eth-tester 地址/哈希仅本地有效。本文不构成投资建议,不引导主网部署。若要用钱包做链上操作,可另见 欧易 Web3 钱包教程(推荐码 CHENGXULVTU 写在创建步骤即可)。
