Solidity智能合约从入门到精通:基于Remix实现第一个可运行合约
很多人觉得智能合约开发门槛很高,要懂区块链底层、要会密码学、要搭本地节点。其实完全不用这么复杂——Solidity语法和JavaScript、C#高度相似,Remix在线编辑器开箱即用,十分钟就能写出并部署第一个真正运行在链上的合约。
做过几年合约开发,见过太多新手一开始就钻进底层原理,结果半个月还没跑通一个Hello World。学习合约最有效的路径,是先跑起来,再慢慢挖深。今天从环境、语法到完整部署,手把手带你实现第一个可运行的智能合约,附踩坑清单和进阶路线。
一、先搞懂三个核心概念
写合约之前,先把三个最核心的概念掰明白,后面所有代码都是围绕这三点展开。
- 账户:以太坊上有两种账户,外部账户(普通人的钱包地址)和合约账户(部署后生成的地址)。两者都能存ETH、收发交易,区别是合约账户由代码控制,没有私钥。
- 交易:调用合约方法不是本地执行,而是发一笔交易到链上,由全网节点共同执行并记账,执行结果不可逆转。
- Gas:链上计算和存储都要消耗Gas,相当于手续费。逻辑越复杂、存储越多,Gas费越高,所以合约开发永远追求精简。
记住一句话:合约一旦部署,代码就不能改;出了问题不能回滚,丢了钱没人能找回。 这是合约开发和传统后端开发最大的区别。
二、开发环境:Remix开箱即用
不用装Node.js、不用搭本地节点、不用配Truffle/Hardhat,新手入门直接用Remix——官方推出的在线IDE,浏览器打开就能写、能编译、能部署、能调试。
打开地址:remix.ethereum.org,界面分成三大区域:
默认环境下有几个示例合约,可以直接删了自己写。部署方式分三种:
- Remix虚拟机:本地模拟链环境,不用真实Gas,适合调试逻辑,首选用这个
- 测试网:连接MetaMask钱包,部署到Goerli/Sepolia测试网,和真实链环境一致
- 主网:真实资产环境,新手不要碰
入门阶段全程用Remix虚拟机就行,零成本、秒出块,调试完再上测试网。
三、第一个合约:实现一个商品存证合约
我们不写无聊的Hello World,直接实现一个有实际业务意义的合约——商品溯源存证合约。功能很简单:商家可以登记商品信息,任何人都可以查询验证,数据上链后不可篡改。
3.1 完整合约代码
新建一个文件 ProductTrace.sol,直接复制以下代码,每一行都有注释:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title 商品溯源存证合约
* @dev 实现商品信息的链上登记与查询
*/
contract ProductTrace {
// 定义商品结构体
struct Product {
uint256 productId; // 商品ID
string name; // 商品名称
string batchNo; // 生产批次
uint256 produceTime; // 生产时间戳
address producer; // 生产商地址
bool exists; // 是否存在
}
// 商品ID => 商品信息 的映射存储
mapping(uint256 => Product) private products;
// 事件:商品登记成功时触发
event ProductRegistered(
uint256 indexed productId,
string name,
address indexed producer
);
/**
* @dev 登记新商品
* @param _productId 商品ID
* @param _name 商品名称
* @param _batchNo 生产批次
*/
function registerProduct(
uint256 _productId,
string calldata _name,
string calldata _batchNo
) external {
// 校验:商品不能重复登记
require(!products[_productId].exists, "Product already exists");
// 写入存储
products[_productId] = Product({
productId: _productId,
name: _name,
batchNo: _batchNo,
produceTime: block.timestamp,
producer: msg.sender,
exists: true
});
// 触发事件
emit ProductRegistered(_productId, _name, msg.sender);
}
/**
* @dev 查询商品信息
* @param _productId 商品ID
*/
function getProduct(uint256 _productId)
external
view
returns (
string memory name,
string memory batchNo,
uint256 produceTime,
address producer
)
{
Product storage p = products[_productId];
require(p.exists, "Product not found");
return (p.name, p.batchNo, p.produceTime, p.producer);
}
}
3.2 逐行拆解核心语法
这一个合约覆盖了Solidity 80%的常用语法,搞懂它就算入门了。
第一行:SPDX-License-Identifier
开源协议声明,Solidity 0.6.8之后强制要求,不写编译会告警。一般写MIT就行。
第二行:pragma solidity ^0.8.20
指定编译器版本。^表示兼容0.8.20及以上、0.9.0以下的版本。0.8.0是个重要分界点——从此版本开始,算术运算默认自带溢出检查,溢出直接回滚交易。
结构体 struct
和C语言、Go的结构体概念一样,用来定义自定义数据结构。注意字符串类型默认是storage存储,传参时用calldata可以省Gas。
映射 mapping
相当于其他语言的字典/哈希表,mapping(Key => Value)。链上存储最核心的数据结构,查找是O(1)复杂度,但不能遍历。要遍历的话,一般额外维护一个数组存所有Key。
函数可见性
external:只能从外部调用,合约内部不能直接调,Gas最低public:内外都能调用,自动生成getterinternal:只能内部和继承合约调用private:只有当前合约内部可访问
view 与 pure
view:只读不写,不修改链上状态,调用不用花Gaspure:既不读也不写状态,纯计算,也不用Gas- 什么都不加:会修改状态,必须发交易,消耗Gas
require 校验
条件不满足就回滚交易,同时返回错误信息。是合约里最常用的校验方式,所有入参合法性、权限判断都用它。
事件 event
合约不能主动往外发消息,事件就是链上日志。前端可以监听事件做业务回调,也可以用来存一些不需要链上读取、只需要留痕的数据——比存storage便宜得多。
四、编译与部署:十分钟跑通全流程
4.1 编译合约
- 右侧面板切换到「Solidity Compiler」标签
- 编译器版本选0.8.20(和代码里pragma对应)
- 点击「Compile ProductTrace.sol」
- 出现绿色对勾就是编译成功,红色报错看下方提示
常见编译坑:
- 版本不匹配:编译器版本要大于等于pragma指定的最低版本
- 中文注释乱码:文件编码必须是UTF-8
- 语法错误:0.8之后
constructor不用加public,旧写法会报错
4.2 部署到虚拟机
- 右侧切换到「Deploy & Run Transactions」
- Environment选「Remix VM (London)」,这是本地模拟环境
- 账户下拉框里有10个测试账户,每个都有100ETH测试币
- 点击「Deploy」,下方控制台会出现部署交易记录
- 部署成功后,下方「Deployed Contracts」里就出现了合约实例
4.3 交互测试
部署好的合约可以直接在Remix里调用:
- 登记商品:在
registerProduct方法输入框里填入参数,格式:1, "有机牛奶", "B20240501",点击按钮发交易 - 查询商品:在
getProduct方法里填入商品ID1,点击就能看到返回结果 - 查看事件:点开控制台的交易详情,在Logs里能看到我们定义的
ProductRegistered事件
试着重复登记同一个ID,交易会失败并返回我们写的错误提示「Product already exists」——这就是链上校验的效果。
五、进阶:加权限、加流转、避大坑
跑通基础版之后,我们再加点实用功能,顺便讲几个新手最容易踩的坑。
5.1 增加管理员权限
不是谁都能登记商品,只有认证商家才行。我们加一个所有者权限控制:
contract ProductTrace {
address public owner;
// 构造函数:部署时执行一次
constructor() {
owner = msg.sender;
}
// 修饰器:权限校验
modifier onlyOwner() {
require(msg.sender == owner, "Not authorized");
_; // 占位符,代表原函数代码
}
// 给登记方法加上权限修饰器
function registerProduct(...) external onlyOwner {
// ...
}
}
modifier修饰器是Solidity的特色语法,相当于AOP切面,把校验逻辑抽出来复用。_代表原函数的代码执行位置。
5.2 增加商品流转功能
再加上物流流转功能,每次流转都记录在链上,形成完整链路:
struct TransferRecord {
uint256 timestamp;
address from;
address to;
string location;
}
// 每个商品对应一个流转记录数组
mapping(uint256 => TransferRecord[]) private transferHistory;
function transferProduct(
uint256 _productId,
address _to,
string calldata _location
) external onlyOwner {
Product storage p = products[_productId];
require(p.exists, "Product not found");
transferHistory[_productId].push(TransferRecord({
timestamp: block.timestamp,
from: msg.sender,
to: _to,
location: _location
}));
}
5.3 新手必踩的五个坑
第一个坑:数组越界不报错,但会直接回滚
Solidity里访问数组下标越界,不会返回-1也不会抛异常,而是整个交易直接回滚。查之前一定要先判断长度。
第二个坑:string不能直接比较
不能写 if (a == b) 比较两个字符串,正确做法是先转成keccak256哈希再比:
keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b))
第三个坑:小数运算不存在
Solidity没有浮点数,所有金额都是以最小单位计算。ETH的最小单位是wei,1ETH = 10^18 wei。涉及百分比、利率都要用整数乘除法实现。
第四个坑:重入攻击
如果合约里有转账功能msg.sender.call{value: amount}(""),转账后对方合约可以在fallback函数里反复调用提款方法,把钱转空。标准解法是「先改状态,再转账」。
第五个坑:随机数不安全block.timestamp、block.difficulty这些区块变量都不是真正随机的,矿工可以在一定范围内操纵。不能用来做抽奖、游戏开奖。
六、学习路线:从入门到生产级
搞定第一个合约之后,想往深了学,按这个路线走最扎实:
- 基础语法阶段:把数据类型、存储位置、继承、接口、异常处理吃透,写三五个小合约练手
- 开发工具阶段:从Remix过渡到Hardhat/Truffle,学会本地测试、脚本部署、单元测试
- 安全规范阶段:学习OpenZeppelin标准库,掌握常见漏洞原理和防护方案
- 优化进阶阶段:Gas优化、升级合约设计、代理模式、跨链桥原理
- 业务落地阶段:ERC20、ERC721、DAO、DeFi协议源码精读
强烈建议:所有合约开发都基于OpenZeppelin标准库来写,权限、代币、升级、安全工具都封装好了,不要自己造轮子——自己造的轮子,十有八九有安全漏洞。
七、最后说几句
Solidity语法本身不难,有其他语言基础的人一周就能上手写业务。难的是思维方式的转变——传统后端写崩了可以重启、可以回滚、可以修数据;智能合约不行,一次漏洞可能就是百万级损失。
所以写合约永远记住三个原则:逻辑越简单越好、依赖越少越好、测试越全越好。先保证安全,再谈功能丰富。
建议新手先用Remix把基础合约写熟,再上测试网真实部署一次,感受一下Gas费、区块确认、交易回执这些真实链上概念。等对链上执行模型有了体感,再去学复杂框架和高级特性,效率会高很多。
合规提示:本文涉及的智能合约技术仅用于技术学习与联盟链业务场景研究,智能合约部署与运行需遵守所在国家和地区相关法律法规,不得用于虚拟货币发行、融资及交易等违规活动。
更多推荐
所有评论(0)