概述

Solidity是以太坊智能合约的主要编程语言,是一种静态类型、面向合约的高级语言。本文整理了Solidity开发中的核心概念,包括数据类型、地址操作、ABI编码、存储机制、Gas优化以及函数修饰符等关键知识点,适合初学者系统学习和开发者快速参考。


一、基本数据类型

1.1 整型(Integer Types)

Solidity提供有符号整型(int)和无符号整型(uint),支持8到256位的多种长度:

类型 别名 范围
uint uint256 0 到 2²⁵⁶-1
int int256 -2²⁵⁵ 到 2²⁵⁵-1
uint8 - 0 到 255
uint32 - 0 到 2³²-1
uint128 - 0 到 2¹²⁸-1

重要提示:在Solidity 0.8.0之前,整型运算溢出会被静默截断(truncate)。从0.8.0开始,溢出会自动触发revert。如需旧版行为,可使用unchecked块。

// Solidity 0.8.0+ 溢出处理示例
uint8 a = 255;
uint8 b = a + 1;  // 会revert

unchecked {
    uint8 c = a + 1;  // c = 0,溢出回绕
}

1.2 布尔型(Boolean)

bool public isActive = true;
bool public isPaused = false;

// 支持的运算符:!、&&、||、==、!=

1.3 字节类型(Bytes)

类型 说明 使用场景
bytes1 到 bytes32 固定长度字节数组 存储哈希值、签名等
bytes 动态长度字节数组 任意长度二进制数据
string 动态长度UTF-8字符串 文本数据
bytes32 public hash = keccak256("hello");
bytes public data = hex"001122";
string public name = "MyToken";

二、地址类型详解

2.1 address vs address payable

Solidity中的地址类型用于存储20字节的以太坊地址,分为两种:

类型 说明 可用成员
address 普通地址,不能直接接收ETH balancecodecodehashcalldelegatecallstaticcall
address payable 可支付地址,可以接收ETH 上述所有 + transfersend

2.2 ETH转账方法对比

方法 Gas限制 失败处理 推荐程度
transfer() 2300 gas 自动revert ⚠️ 不推荐(Gas不足风险)
send() 2300 gas 返回bool ⚠️ 不推荐
call{value:}("") 可配置 返回(bool, bytes) ✅ 推荐使用

2.3 地址操作完整示例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract AddressDemo {
    address public owner;
    address payable public treasury;
    
    // 记录各地址余额快照
    uint public ownerBalance;
    uint public treasuryBalance;
    uint public contractBalance;
    
    constructor() payable {
        owner = msg.sender;
        treasury = payable(0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2);
        _updateBalances();
    }
    
    // 使用transfer转账(不推荐,Gas固定2300)
    function sendWithTransfer() public payable {
        treasury.transfer(1 ether);
        _updateBalances();
    }
    
    // 使用call转账(推荐方式)
    function sendWithCall() public payable {
        (bool success, ) = treasury.call{value: 1 ether}("");
        require(success, "Transfer failed");
        _updateBalances();
    }
    
    // 更新余额快照
    function _updateBalances() private {
        ownerBalance = owner.balance;
        treasuryBalance = treasury.balance;
        contractBalance = address(this).balance;
    }
    
    // 接收ETH
    receive() external payable {}
}

三、ABI编码与函数选择器

3.1 什么是ABI?

ABI(Application Binary Interface,应用二进制接口)是以太坊生态系统中与智能合约交互的标准接口。它定义了:

  • 如何编码函数调用数据
  • 如何解码返回值
  • 事件的编码格式

3.2 函数选择器(Function Selector)

函数调用数据的前4个字节是函数选择器,用于标识要调用的函数:

// 函数选择器 = keccak256("函数签名")的前4字节
bytes4 selector = bytes4(keccak256("transfer(address,uint256)"));
// selector = 0xa9059cbb

3.3 ABI编码函数

函数 说明 使用场景
abi.encode(...) 标准ABI编码 一般数据编码
abi.encodePacked(...) 紧凑编码,无填充 哈希计算、节省空间
abi.encodeWithSelector(selector, ...) 带选择器的编码 底层调用
abi.encodeWithSignature(sig, ...) 带签名的编码 底层调用
abi.decode(data, (types)) 解码ABI数据 解析返回值

3.4 安全转账实现示例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SafeTransferDemo {
    address public owner;
    mapping(address => uint256) public balances;
    
    // 预计算函数选择器,节省Gas
    bytes4 private constant TRANSFER_SELECTOR = 
        bytes4(keccak256(bytes("transfer(address,uint256)")));
    
    constructor() {
        owner = msg.sender;
        balances[msg.sender] = 10000;
        balances[address(this)] = 10000;
    }
    
    // 标准转账函数
    function transfer(address to, uint256 value) public returns (bool) {
        require(balances[msg.sender] >= value, "Insufficient balance");
        balances[msg.sender] -= value;
        balances[to] += value;
        return true;
    }
    
    // 安全转账:通过底层call调用,处理返回值
    function _safeTransfer(
        address token, 
        address to, 
        uint256 value
    ) internal {
        // 编码调用数据
        (bool success, bytes memory data) = token.call(
            abi.encodeWithSelector(TRANSFER_SELECTOR, to, value)
        );
        
        // 检查调用是否成功
        // 兼容不返回值的代币(如USDT)
        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            "Transfer failed"
        );
    }
    
    // 测试安全转账
    function testSafeTransfer() public {
        _safeTransfer(
            address(this),
            0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2,
            100
        );
    }
}

四、CREATE2与合约部署

4.1 CREATE vs CREATE2

特性 CREATE CREATE2
地址计算 sender + nonce sender + salt + bytecode
地址可预测 ❌ 依赖nonce ✅ 完全确定
使用场景 普通部署 工厂合约、状态通道

4.2 CREATE2地址计算公式

address = keccak256(0xff ++ deployerAddress ++ salt ++ keccak256(bytecode))[12:]

4.3 type(C).creationCode

type(C).creationCode返回合约C的创建字节码,常用于CREATE2部署:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// 交易对合约
contract Pair {
    address public token0;
    address public token1;
    
    function initialize(address _token0, address _token1) external {
        token0 = _token0;
        token1 = _token1;
    }
}

// 工厂合约
contract PairFactory {
    mapping(address => mapping(address => address)) public getPair;
    address[] public allPairs;
    
    // 预计算INIT_CODE_HASH,用于链下计算Pair地址
    bytes32 public constant INIT_CODE_HASH = 
        keccak256(abi.encodePacked(type(Pair).creationCode));
    
    event PairCreated(
        address indexed token0, 
        address indexed token1, 
        address pair, 
        uint256 index
    );
    
    function createPair(
        address tokenA, 
        address tokenB
    ) external returns (address pair) {
        require(tokenA != tokenB, "Identical addresses");
        
        // 排序token地址,确保唯一性
        (address token0, address token1) = tokenA < tokenB 
            ? (tokenA, tokenB) 
            : (tokenB, tokenA);
            
        require(token0 != address(0), "Zero address");
        require(getPair[token0][token1] == address(0), "Pair exists");
        
        // 获取Pair合约的创建字节码
        bytes memory bytecode = type(Pair).creationCode;
        
        // 使用两个token地址作为salt
        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        
        // 使用CREATE2部署合约
        assembly {
            pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
        }
        
        // 初始化Pair合约
        Pair(pair).initialize(token0, token1);
        
        // 更新映射
        getPair[token0][token1] = pair;
        getPair[token1][token0] = pair;
        allPairs.push(pair);
        
        emit PairCreated(token0, token1, pair, allPairs.length);
    }
    
    // 链下计算Pair地址(无需调用合约)
    function computePairAddress(
        address token0, 
        address token1
    ) external view returns (address) {
        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        return address(uint160(uint256(keccak256(abi.encodePacked(
            bytes1(0xff),
            address(this),
            salt,
            INIT_CODE_HASH
        )))));
    }
}

五、数据存储位置

5.1 Storage vs Memory vs Calldata

位置 持久性 Gas成本 使用场景
storage 永久存储在区块链 高(SSTORE: 20000 gas) 状态变量
memory 函数执行期间 中等 函数内临时变量
calldata 只读,函数执行期间 最低 external函数参数

5.2 存储位置规则

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract StorageDemo {
    // 状态变量:自动存储在storage
    string public name = "StorageDemo";
    uint256[] public numbers;
    
    struct User {
        string name;
        uint256 balance;
    }
    mapping(address => User) public users;
    
    // 函数参数:string/bytes/数组/结构体需要指定位置
    function updateName(string memory newName) public {
        // memory变量:函数内临时使用
        string memory tempName = newName;
        
        // 写入storage(永久保存)
        name = tempName;
    }
    
    // calldata:只读,Gas更低
    function processData(string calldata data) external pure returns (uint256) {
        return bytes(data).length;
    }
    
    // storage引用:直接修改状态变量
    function updateUser(address addr, uint256 newBalance) public {
        // 获取storage引用,修改会直接影响状态
        User storage user = users[addr];
        user.balance = newBalance;
    }
    
    // memory复制:修改不影响原数据
    function getUserCopy(address addr) public view returns (User memory) {
        // 复制到memory,返回副本
        User memory user = users[addr];
        return user;
    }
}

5.3 常见错误与解决方案

// ❌ 错误:值类型不能指定memory
// uint memory var1 = 0;  
// TypeError: Storage location can only be given for array or struct types.

// ✅ 正确:值类型直接声明
uint256 var1 = 0;

// ❌ 错误:函数内string未指定位置
// function test(string name) { }
// TypeError: Data location must be specified

// ✅ 正确:指定memory或calldata
function test1(string memory name) public { }
function test2(string calldata name) external { }

六、函数修饰符详解

6.1 view、pure、payable对比

修饰符 读取状态 修改状态 接收ETH Gas消耗
(无) 需要Gas
view 外部调用免费
pure 外部调用免费
payable 需要Gas

6.2 view函数

view函数承诺只读取状态,不修改状态:

contract ViewDemo {
    uint256 public value = 100;
    
    // view函数:可以读取状态变量
    function getValue() public view returns (uint256) {
        return value;
    }
    
    // view函数不能做的事:
    // - 写入状态变量
    // - 发出事件
    // - 创建合约
    // - 使用selfdestruct
    // - 发送ETH
    // - 调用非view/pure函数
}

6.3 pure函数

pure函数是最严格的,既不读取也不修改状态:

// SafeMath库:所有函数都是pure的
library SafeMath {
    function add(uint256 x, uint256 y) internal pure returns (uint256 z) {
        require((z = x + y) >= x, "SafeMath: addition overflow");
    }

    function sub(uint256 x, uint256 y) internal pure returns (uint256 z) {
        require((z = x - y) <= x, "SafeMath: subtraction underflow");
    }

    function mul(uint256 x, uint256 y) internal pure returns (uint256 z) {
        require(y == 0 || (z = x * y) / y == x, "SafeMath: multiplication overflow");
    }
    
    function div(uint256 x, uint256 y) internal pure returns (uint256 z) {
        require(y > 0, "SafeMath: division by zero");
        z = x / y;
    }
}

contract PureDemo {
    // pure函数不能做的事(除了view的限制外):
    // - 读取状态变量
    // - 访问address(this).balance
    // - 访问block、tx、msg的成员(msg.sig和msg.data除外)
    // - 调用非pure函数
    
    function calculate(uint256 a, uint256 b) public pure returns (uint256) {
        return a * b + a / b;
    }
    
    function hash(string memory input) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(input));
    }
}

6.4 payable函数

contract PayableDemo {
    event Received(address sender, uint256 amount);
    
    // payable构造函数:部署时可以发送ETH
    constructor() payable { }
    
    // payable函数:可以接收ETH
    function deposit() public payable {
        emit Received(msg.sender, msg.value);
    }
    
    // receive函数:接收纯ETH转账
    receive() external payable {
        emit Received(msg.sender, msg.value);
    }
    
    // fallback函数:处理未匹配的调用
    fallback() external payable {
        emit Received(msg.sender, msg.value);
    }
}

七、Gas估算与优化

7.1 Gas估算方法

在Remix中估算Gas:

// Remix控制台脚本
(async () => {
    try {
        const result = await web3.eth.estimateGas({
            to: "0xContractAddress",
            data: "0xFunctionSelectorAndParams"
        });
        console.log("Estimated Gas:", result);
    } catch (e) {
        console.log("Error:", e.message);
    }
})();

在Node.js中估算Gas:

const Web3 = require('web3');
const web3 = new Web3('https://mainnet.infura.io/v3/YOUR_PROJECT_ID');

async function estimateGas() {
    const gas = await web3.eth.estimateGas({
        to: "0xContractAddress",
        data: "0xFunctionSelectorAndParams"
    });
    console.log("Estimated Gas:", gas);
}

estimateGas();

7.2 Gas优化技巧

优化技巧 说明 Gas节省
使用calldata替代memory 只读参数使用calldata ~60 gas/参数
打包存储变量 将小于32字节的变量放在一起 ~20000 gas/slot
使用++i替代i++ 前置递增更高效 ~5 gas/次
缓存数组长度 循环前缓存array.length ~100 gas/循环
使用immutable 部署时设置,后续读取便宜 ~200 gas/读取
使用constant 编译时常量 ~200 gas/读取
使用unchecked 跳过溢出检查(确保安全时) ~40 gas/运算
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GasOptimization {
    // ✅ 使用constant和immutable
    uint256 public constant FIXED_VALUE = 100;
    address public immutable OWNER;
    
    uint256[] public data;
    
    constructor() {
        OWNER = msg.sender;
    }
    
    // ❌ 未优化版本
    function sumBad(uint256[] memory arr) public pure returns (uint256) {
        uint256 total = 0;
        for (uint256 i = 0; i < arr.length; i++) {
            total = total + arr[i];
        }
        return total;
    }
    
    // ✅ 优化版本
    function sumGood(uint256[] calldata arr) public pure returns (uint256) {
        uint256 total = 0;
        uint256 len = arr.length;  // 缓存长度
        for (uint256 i = 0; i < len; ) {
            unchecked {
                total += arr[i];
                ++i;  // 前置递增
            }
        }
        return total;
    }
}

八、常用全局变量

8.1 区块和交易属性

变量 类型 说明
block.number uint256 当前区块号
block.timestamp uint256 当前区块时间戳(秒)
block.basefee uint256 当前区块基础费用
block.chainid uint256 链ID
block.coinbase address payable 区块矿工地址
msg.sender address 消息发送者
msg.value uint256 发送的ETH数量(wei)
msg.data bytes calldata 完整的calldata
msg.sig bytes4 函数选择器
tx.origin address 交易发起者(不推荐使用)
tx.gasprice uint256 交易Gas价格
gasleft() uint256 剩余Gas

九、参考资源


总结

本文涵盖了Solidity开发的核心知识点:

  1. 数据类型:理解整型范围和溢出处理,正确使用字节和字符串类型
  2. 地址操作:区分addressaddress payable,优先使用call进行ETH转账
  3. ABI编码:掌握函数选择器计算和各种编码函数的使用场景
  4. CREATE2:理解确定性地址部署的原理和应用
  5. 存储位置:正确选择storagememorycalldata
  6. 函数修饰符:合理使用viewpurepayable优化Gas消耗
  7. Gas优化:应用各种技巧降低合约执行成本

掌握这些基础知识,将为进一步学习DeFi协议、NFT开发和高级智能合约设计打下坚实基础。