2、Solidity基础回顾:数据类型与变量、函数修饰符、事件与日志、地址与转账

各位同学,咱们今天聊聊Solidity的基础。说实话,很多安全漏洞的根源,就是对这些基础概念理解得不够透彻。我见过太多项目因为一个public变量没处理好,或者转账方式选错了,直接导致几百万美金打了水漂。所以这一节,咱们把地基打牢。

2.1 数据类型与变量:存储位置决定命运

Solidity里的数据类型,说白了就分两大类:值类型引用类型

值类型包括boolintuintaddressbytes1~bytes32这些。它们的特点是——每次赋值都是拷贝一份新的。你改你的,我改我的,互不干扰。

引用类型就复杂了,包括stringbytes数组结构体映射。它们的特点是——赋值只是传了个指针。你改我也改,容易出乱子。

关键点:引用类型必须指定data location,也就是数据存在哪。三个选项:storage(链上)、memory(内存)、calldata(只读输入)。

我个人习惯,写函数参数时能用calldata就用calldata,能省gas。但要注意,calldata是只读的,不能修改。

// 值类型示例
uint256 public myNumber = 42;
address public owner = msg.sender;

// 引用类型示例
struct User {
    string name;
    uint256 age;
}

// 注意:storage和memory的区别
function updateUser(User storage _user) internal {
    _user.age += 1;  // 直接修改链上数据
}

function createUser(string memory _name, uint256 _age) public returns (User memory) {
    return User(_name, _age);  // 返回内存中的临时数据
}

⚠️ 避坑指南:我曾经审计过一个项目,开发者在函数里把storagememory搞混了。他想修改一个状态变量,结果因为用了memory,改了半天改的是临时副本,链上数据纹丝不动。用户以为功能生效了,实际上资金一直卡在合约里。

2.2 函数修饰符:权限控制的第一道防线

函数修饰符,说白了就是给函数加个「门禁」。最常见的当然是onlyOwner,但很多人用得不够严谨。

Solidity里内置的修饰符有:

  • public:谁都能调用
  • private:只有本合约能调用
  • internal:本合约和子合约能调用
  • external:只能从外部调用,内部调用会报错

嗯,这里要注意:externalpublic省gas,因为参数直接读calldata。但如果你在合约内部想调用一个external函数,得用this.func()的方式,这会触发一次外部调用,反而更贵。

// 自定义修饰符示例
address public owner;

modifier onlyOwner() {
    require(msg.sender == owner, "你不是合约所有者");
    _;  // 继续执行原函数
}

function withdraw() public onlyOwner {
    // 只有owner能调用
    payable(owner).transfer(address(this).balance);
}

// 带参数的修饰符
modifier cost(uint256 _amount) {
    require(msg.value >= _amount, "以太币不够");
    _;
}

function buyItem(uint256 _price) public payable cost(_price) {
    // 自动检查msg.value是否足够
}

💡 个人经验:我建议每个合约都加上onlyOwner修饰符,但别滥用。有些功能其实应该用多签或者时间锁来控制。我曾经见过一个项目,owner可以随时提取所有用户资金,虽然代码没漏洞,但信任模型本身就是个炸弹。

2.3 事件与日志:链上的「小纸条」

事件(Event)是Solidity里一个很巧妙的设计。它不存数据,只存日志。说白了就是合约在链上贴了个「小纸条」,告诉外部世界:「嘿,刚才发生了这件事!」

为什么需要事件?因为合约里的状态变量虽然能查到,但查历史变化很麻烦。事件就是专门用来记录「发生了什么」的。

// 定义事件
event Transfer(address indexed from, address indexed to, uint256 value);
event Deposit(address indexed user, uint256 amount, uint256 timestamp);

// 触发事件
function transfer(address _to, uint256 _value) public returns (bool) {
    require(balanceOf[msg.sender] >= _value, "余额不足");
    
    balanceOf[msg.sender] -= _value;
    balanceOf[_to] += _value;
    
    emit Transfer(msg.sender, _to, _value);  // 记录转账事件
    return true;
}

注意indexed关键字。加了indexed的参数,可以被外部程序高效检索。最多三个参数能加indexed。我一般把最重要的参数(比如发送方、接收方)加上索引。

安全要点:事件是廉价的,但也是不可篡改的。一旦上链,任何人都能查到。所以别在事件里记录敏感信息,比如密码、私钥之类的。你想想看,这等于把秘密写在公告栏上。

2.4 地址与转账:以太坊的「钱袋子」

地址类型在Solidity里很特殊。它既是身份标识,也是「钱袋子」。每个地址都有一个balance属性,表示它有多少以太币。

转账有三种方式:

方法 Gas限制 安全性 推荐场景
address.transfer() 固定2300 gas 高(自动回退) 简单转账,接收方是普通地址
address.send() 固定2300 gas 中(返回bool) 需要检查是否成功的场景
address.call{value: x}("") 可自定义 低(需手动处理重入) 需要调用接收方函数的场景

我个人强烈建议:能用transfer就别用call。为什么?因为call会把所有剩余gas转发给接收方,如果接收方是个合约,它可以在fallback函数里做任何事,包括重入攻击。

// 安全的转账方式
function safeWithdraw(uint256 _amount) public {
    require(_amount <= balances[msg.sender], "余额不足");
    balances[msg.sender] -= _amount;
    
    // 使用transfer,gas限制2300,防止重入
    payable(msg.sender).transfer(_amount);
}

// 不推荐的转账方式(除非你知道在做什么)
function riskyWithdraw(uint256 _amount) public {
    require(_amount <= balances[msg.sender], "余额不足");
    balances[msg.sender] -= _amount;
    
    // call会把所有gas转发,接收方可以重入
    (bool success, ) = msg.sender.call{value: _amount}("");
    require(success, "转账失败");
}

⚠️ 我曾经踩过的坑:有个项目用call给用户发奖励,结果有个用户部署了一个恶意合约,在fallback函数里又调用了提现函数。因为call转发了所有gas,这个重入循环跑了上百次,直到gas耗尽。虽然没造成资金损失,但交易一直失败,用户体验极差。

嗯,这里还要提一句:address(this).balance可以获取合约当前的以太币余额。但别直接用这个值做判断,因为别人可以通过selfdestruct强制给你合约转币,你的余额会突然增加。

💡 小技巧:如果你需要判断用户是否支付了足够的以太币,用msg.value而不是address(this).balancemsg.value是当前交易附带的以太币,不会被外部强制转账影响。

Solidity 基础核心知识 数据类型与变量 值类型:bool, int, uint, address 引用类型:string, bytes, 数组, 结构体 ⚠️ 必须指定 data location storage / memory / calldata 选错位置 = 改错数据 函数修饰符 可见性:public, private, internal, external 自定义:modifier + require ⚠️ external 比 public 省 gas onlyOwner 是基础,但别滥用 权限控制 = 合约的第一道门 事件与日志 event + emit 记录链上日志 indexed 参数可高效检索 ⚠️ 不可篡改,别记敏感信息 最多3个 indexed 参数 事件 = 链上的「小纸条」 地址与转账 transfer:2300 gas,安全 send:2300 gas,返回 bool ⚠️ call:可自定义 gas,有重入风险 msg.value 判断支付金额 能用 transfer 就别用 call 基础不牢,地动山摇

好了,这一节的内容就这些。记住:数据类型决定怎么存,修饰符决定谁能用,事件决定怎么查,转账决定怎么花。这四个点搞明白了,Solidity基础就算打牢了。


专注资料整理