学习solidity(高级特性)

191 阅读25分钟

学习solidity(高级特性)

常量和不可变变量

1. 赋值时机:

  • 常量(constant): 必须在声明时立即赋值,其值在编译时就确定了。这意味着你不能在构造函数或其他函数中给常量赋值。
  • 不可变变量(immutable): 可以在声明时赋值,也可以在构造函数中赋值。这提供了更大的灵活性,允许你在合约部署时根据一些参数来初始化不可变变量。

2. 存储位置和 Gas 消耗:

  • 常量(constant): 不占用存储空间。每次访问常量时,编译器会将常量的值直接内联到代码中。这意味着每次访问都需要重新计算或读取值(如果值是一个表达式)。虽然这在存储上节省了 Gas,但在某些情况下,如果常量是一个复杂的表达式,可能会导致执行 Gas 略微增加。
  • 不可变变量(immutable): 存储在合约的存储空间中,但在合约部署后就不能再修改。因此,它们只在部署时消耗 Gas 来写入值,之后读取的 Gas 消耗与普通状态变量类似,但比普通状态变量更节省 Gas,因为编译器知道它们的值不会改变,可以进行一些优化。

3. 数据类型:

  • 常量(constant): 只能是值类型,例如 uintaddressstring 字面量等。不能是引用类型,例如数组、结构体或映射。
  • 不可变变量(immutable): 可以是值类型或引用类型。

4. 用途:

  • 常量(constant): 适用于在编译时就已知的固定值,例如一些数学常数、字符串字面量等。
  • 不可变变量(immutable): 适用于在合约部署时根据参数确定的值,例如合约的创建者地址、初始配置值等。

总结表格:

特性常量(constant)不可变变量(immutable)
赋值时机声明时(编译时)声明时或构造函数中(部署时)
存储位置不存储(内联到代码)存储空间
Gas 消耗访问时可能略高,不占用存储部署时写入,后续读取 Gas 较低
数据类型值类型值类型或引用类型
适用场景编译时已知的固定值部署时确定的值

示例:

 pragma solidity ^0.8.0;
 ​
 contract ConstantImmutableExample {
     // 常量,必须在声明时赋值
     uint256 public constant MAX_SUPPLY = 1000000;
     string public constant GREETING = "Hello, world!";
 ​
     // 不可变变量,可以在构造函数中赋值
     address public immutable owner;
     uint256 public immutable creationTime;
 ​
     constructor() {
         owner = msg.sender;
         creationTime = block.timestamp;
     }
 ​
     function test() public view returns (uint256, string memory) {
         // 可以访问常量
         uint256 halfSupply = MAX_SUPPLY / 2;
         // 可以访问不可变变量
         return (creationTime, GREETING);
     }
 }

在这个例子中:

  • MAX_SUPPLYGREETING 是常量,它们的值在编译时就确定了。
  • ownercreationTime 是不可变变量,它们的值在合约部署时,通过构造函数来初始化。

最佳实践:

  • 如果值在编译时已知且不会改变,则使用 constant
  • 如果值需要在合约部署时根据参数确定,则使用 immutable。例如实现一些全局变量赋予
  • 尽可能使用 constantimmutable,而不是普通的 state variable,以节省 Gas 并提高代码的可读性。 同时也要保证变量的运用的效率
  • 同时我们在设计成员变量时加上的public会自动实现一个get方法

通过理解常量和不可变变量之间的区别,你可以更有效地编写 Solidity 智能合约,并更好地控制 Gas 消耗。希望以上解释能够帮助你更好地理解它们。

两类特殊的函数

在 Solidity 中,receive()fallback() 是两种特殊的函数,它们用于处理合约接收以太币(ETH)以及处理未匹配的函数调用。它们在合约交互中扮演着重要的角色,理解它们的工作方式对于编写健壮的智能合约至关重要。

背景:

在 Solidity 0.6.0 版本之前,只有一个 fallback() 函数负责处理所有接收 ETH 的情况以及未匹配的函数调用。为了更好地组织代码和提高安全性,Solidity 0.6.0 引入了 receive() 函数,专门用于处理接收 ETH 的情况。

receive() 函数:

  • 用途: 专门用于接收以太币的函数。当合约收到没有附加任何数据(msg.data 为空)的以太币转账时,receive() 函数会被自动调用。

  • 语法:

    Solidity

     receive() external payable {
         // 处理接收 ETH 的逻辑
     }
    
  • 特点:

    • 必须声明为 externalpayable
    • 不能有参数,也不能有返回值。
    • 如果合约中定义了 receive() 函数,并且收到的交易 msg.data 为空,则优先执行 receive() 函数。
    • 如果合约没有定义 receive() 函数,但定义了 fallback() 函数,且 fallback() 函数是 payable 的,则接收 ETH 的操作会调用 fallback() 函数。
    • 如果合约既没有定义 receive() 函数,也没有定义 payablefallback() 函数,则合约无法直接接收 ETH,向该合约发送 ETH 的交易会 revert。

fallback() 函数:

  • 用途: 用于处理以下两种情况:

    • 当合约收到调用,但没有找到匹配的函数签名时,fallback() 函数会被调用。
    • 当合约收到附加了数据的以太币转账时(msg.data 不为空),即使定义了 receive() 函数,也会调用 fallback() 函数。
  • 语法:

    Solidity

     fallback() external [payable] {
         // 处理未匹配的函数调用或附加数据的 ETH 转账的逻辑
     }
    
  • 特点:

    • 必须声明为 external
    • 可以声明为 payable 或非 payable
    • 如果没有其他函数匹配调用,则执行此函数。
    • 如果需要接收 ETH,则必须声明为 payable
    • fallback() 函数最多可以消耗 2300 gas。如果执行逻辑超过这个限制,交易会 revert。

receive() 和 fallback() 的区别总结:

特性receive()fallback()
用途专门接收 ETH(msg.data 为空)处理未匹配的函数调用和附加数据的 ETH 转账
msg.data必须为空可以为空或不为空
payable必须可选,但接收 ETH 时必须
参数和返回值没有没有
gas 限制没有特殊限制,但受区块 gas limit 限制最多 2300 gas(用于发送 ETH 的情况)
Solidity 版本0.6.0 及以上版本所有版本

使用场景和示例:

1. 接收 ETH:

Solidity

 pragma solidity ^0.8.0;
 ​
 contract ReceiveFallbackExample {
     uint256 public balance;
 ​
     receive() external payable {
         balance += msg.value;
         emit Received(msg.sender, msg.value);
     }
 ​
     fallback() external payable {
         balance += msg.value;
         emit FallbackCalled(msg.sender, msg.value);
     }
 ​
     event Received(address sender, uint256 amount);
     event FallbackCalled(address sender, uint256 amount);
 }

在这个例子中:

  • 如果向合约发送 ETH 且 msg.data 为空,则 receive() 函数会被调用。
  • 如果向合约发送 ETH 且 msg.data 不为空,或者调用了合约中不存在的函数,则 fallback() 函数会被调用。

2. 代理合约(Proxy Contract):

fallback() 函数常用于实现代理合约模式。代理合约会将所有未匹配的函数调用转发给另一个合约(逻辑合约)。

Solidity

 pragma solidity ^0.8.0;
 ​
 interface Logic {
     function doSomething(uint256 _value) external;
 }
 ​
 contract Proxy {
     address public logicContract;
 ​
     constructor(address _logicContract) {
         logicContract = _logicContract;
     }
 ​
     fallback() external payable {
         (bool success, bytes memory data) = logicContract.delegatecall(msg.data);
         require(success, "Delegate call failed");
 ​
         // assembly is needed to correctly return the data
         assembly {
           return(add(data, 0x20), mload(data))
         }
     }
 }

在这个例子中,Proxy 合约的 fallback() 函数使用 delegatecall 将所有调用转发给 logicContract

关键注意事项:

  • gas 限制: fallback() 函数在接收 ETH 时有 2300 gas 的限制。这意味着你不能在 fallback() 函数中执行复杂的逻辑,否则交易会 revert。通常,只在 fallback() 函数中记录事件或简单地转移 ETH。
  • 重入攻击: 如果在 fallback() 函数中执行了外部调用,需要注意防范重入攻击。
  • receive() 的 gas 消耗: 虽然 receive() 函数本身没有 gas 限制,但是它仍然会消耗 gas,包括基本的交易 gas 消耗以及事件日志的 gas 消耗。

总结:

receive()fallback() 是 Solidity 中处理外部交互的重要组成部分。receive() 专门用于接收 ETH,而 fallback() 用于处理未匹配的函数调用和附加数据的 ETH 转账。理解它们之间的区别和使用场景,可以帮助你编写更安全、更灵活的智能合约。

补充说明

什么时候msg.data为空?

总结:

情况msg.data调用的函数
直接发送 ETH 到合约地址receive() (如果存在) 或 fallback() (如果 payable)
使用 transfer()/send() 发送 ETH (目标合约无参数函数)receive() (如果存在) 或 fallback() (如果 payable)
调用合约的函数不为空匹配的函数或 fallback()
使用 call() 发送数据不为空fallback() 或根据 data 调用其他函数

为什么两类函数都要用 external payable进行修饰

当我们的两类特殊函数被调是都是接受到msg.data为空时,才会触发,换言之从外部调用对应的函数缺少一定的条件时才会触发,我们在内部不使用此类函数,刚好符合对应的external的声明条件呢

自定义修饰符

使用场景

在Solidity中,修饰器(Modifier)是一种强大的工具,用于在函数执行前后添加额外的代码逻辑,常用于权限控制、条件检查和代码复用。它们类似于面向对象编程中的装饰器(Decorator)。

基本语法

修饰器的定义使用 modifier 关键字:

Solidity

 modifier 修饰器名称(参数列表) {
     // 前置代码:在函数主体执行前执行
     _; // 特殊占位符,表示函数主体将在此处执行
     // 后置代码:在函数主体执行后执行
 }

使用方法

在函数定义中使用修饰器,只需在函数名后添加修饰器名称即可:

Solidity

 function 函数名(参数列表) 修饰器名称1 修饰器名称2 {
     // 函数主体
 }

一个函数可以应用多个修饰器,它们将按照声明的顺序依次执行。

使用场景

  1. 权限控制:限制只有特定地址或角色才能调用函数。

    Solidity

     address public owner;
     ​
     constructor() {
         owner = msg.sender;
     }
     ​
     modifier onlyOwner() {
         require(msg.sender == owner, "Only owner can call this function.");
         _;
     }
     ​
     function changeOwner(address _newOwner) public onlyOwner {
         owner = _newOwner;
     }
    
  2. 条件检查:在函数执行前检查某些条件是否满足。

    Solidity

    modifier checkValue(uint _value) {
        require(_value > 0, "Value must be greater than zero.");
        _;
    }
    
    function setValue(uint _value) public checkValue(_value) {
        // ...
    }
    
  3. 代码复用:将通用的逻辑提取到修饰器中,避免代码重复。

    Solidity

    modifier gasCheck() {
        uint startGas = gasleft();
        _;
        uint gasUsed = startGas - gasleft();
        require(gasUsed < 100000, "Gas usage exceeds limit.");
    }
    
    function function1() public gasCheck {
        // ...
    }
    
    function function2() public gasCheck {
        // ...
    }
    

注意事项

  • _ 占位符是必须的,它表示函数主体将在此处执行。如果没有 _,函数主体将不会被执行。
  • 修饰器可以有参数,这些参数可以在修饰器内部使用。
  • 修饰器可以继承和重写,类似于函数。

构造函数修饰器

构造函数修饰器是Solidity中一个非常有用的特性,它允许你在合约的构造函数中调用父合约或其他合约的构造函数,从而实现代码的复用和初始化逻辑的集中管理。它使用起来很像函数修饰器,但作用于构造函数。

基本语法

构造函数修饰器紧跟在构造函数声明之后,使用 contractName(arguments) 的形式调用。

contract ChildContract is ParentContract {
    constructor(uint _x, uint _y) ParentContract(_x) {
        // 子合约的构造函数逻辑
        // ...
    }
}

在这个例子中,ParentContract(_x) 就是一个构造函数修饰器。它调用了 ParentContract 的构造函数,并将 _x 作为参数传递给它。

工作原理

当部署 ChildContract 时,Solidity 编译器会按照以下顺序执行构造函数:

  1. 首先执行父合约(ParentContract)的构造函数。
  2. 然后执行子合约(ChildContract)的构造函数体内的代码。

这意味着,通过构造函数修饰器,你可以确保父合约的初始化逻辑在子合约的初始化逻辑之前执行。

使用场景

  1. 继承合约的初始化: 这是构造函数修饰器最常见的用途。当一个合约继承了另一个合约时,通常需要调用父合约的构造函数来初始化父合约的状态变量。

    Solidity

    contract ParentContract {
        uint public x;
    
        constructor(uint _x) {
            x = _x;
        }
    }
    
    contract ChildContract is ParentContract {
        uint public y;
    
        constructor(uint _x, uint _y) ParentContract(_x) {
            y = _y;
        }
    }
    

    在这个例子中,ChildContract 继承了 ParentContractChildContract 的构造函数使用 ParentContract(_x) 调用了 ParentContract 的构造函数,并将 _x 传递给它,从而初始化了父合约的 x 状态变量。

  2. 多重继承的初始化: 当一个合约继承了多个合约时,可以使用多个构造函数修饰器来调用所有父合约的构造函数。

    Solidity

    contract ContractA {
        uint public a;
        constructor(uint _a) {
            a = _a;
        }
    }
    
    contract ContractB {
        uint public b;
        constructor(uint _b) {
            b = _b;
        }
    }
    
    contract ContractC is ContractA, ContractB {
        uint public c;
        constructor(uint _a, uint _b, uint _c) ContractA(_a) ContractB(_b) {
            c = _c;
        }
    }
    

    在这个例子中,ContractC 继承了 ContractAContractBContractC 的构造函数使用 ContractA(_a)ContractB(_b) 分别调用了 ContractAContractB 的构造函数,从而初始化了所有父合约的状态变量。

  3. 初始化库合约或辅助合约: 虽然库合约不能被继承,但你可以通过部署辅助合约并在构造函数中使用修饰器来初始化它们。这通常用于一些复杂的初始化逻辑,可以将其封装在单独的合约中。

重要注意事项

  • 执行顺序: 构造函数修饰器的执行顺序是从左到右,即先执行最左边的修饰器,然后依次执行后面的修饰器。
  • 我们在调用时最好使父合约与子合约的名称一致,便于理解
  • 参数传递: 可以向构造函数修饰器传递任意数量的参数。
  • 重复调用: 如果一个合约多次继承了同一个合约(例如通过菱形继承),则该合约的构造函数会被多次调用。这可能会导致意想不到的结果,需要谨慎处理。
  • super 的区别: 虽然 super 也可以用于调用父合约的函数,但在构造函数中,应该优先使用构造函数修饰器来调用父合约的构造函数。super 主要用于在子合约中重写父合约的函数并调用父合约的原始实现。

实际运用

  • 修饰器名称:你为修饰器指定的名称,用于在函数中调用。
  • (参数列表):修饰器可以接收参数,就像函数一样。
  • _:这是一个特殊的占位符,表示函数主体将在此处执行。这个下划线是必不可少的,否则函数体将不会被执行。
  • 前置代码:在 _ 之前执行的代码。
  • 后置代码:在 _ 之后执行的代码。

2. 在函数中使用修饰器

在函数定义中使用修饰器,只需在函数名后添加修饰器名称即可:

Solidity

function 函数名(参数列表) 修饰器名称1 修饰器名称2 {
    // 函数主体
}

一个函数可以应用多个修饰器,它们将按照声明的顺序依次执行。

3. 示例:自定义权限控制修饰器

Solidity

pragma solidity ^0.8.0;

contract MyContract {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can call this function.");
        _;
    }

    function changeOwner(address _newOwner) public onlyOwner {
        owner = _newOwner;
    }

    function someOtherFunction() public onlyOwner {
        // 只有 owner 才能执行的代码
    }
}

在这个例子中,onlyOwner 是一个自定义的修饰器,它检查调用者是否是合约的所有者。changeOwnersomeOtherFunction 函数都使用了这个修饰器,这意味着只有合约的所有者才能调用这两个函数。

4. 示例:带参数的修饰器

Solidity

pragma solidity ^0.8.0;

contract MyContract {
    mapping(address => uint) public balances;

    modifier checkBalance(uint _amount) {
        require(balances[msg.sender] >= _amount, "Insufficient balance.");
        _;
    }

    function withdraw(uint _amount) public checkBalance(_amount) {
        balances[msg.sender] -= _amount;
    }
}

在这个例子中,checkBalance 是一个带参数的修饰器,它检查调用者的余额是否足够。withdraw 函数使用了这个修饰器,并传递了需要提取的金额作为参数。

5. 多个修饰器的执行顺序

如果一个函数应用了多个修饰器,它们将按照声明的顺序依次执行。例如:

Solidity

function myFunction() public modifier1 modifier2 {
    // 函数主体
}

在这个例子中,modifier1 将在 modifier2 之前执行。

总结

通过自定义修饰器,你可以有效地组织和复用 Solidity 代码,提高代码的可读性和可维护性,并增强合约的安全性。记住 _ 占位符的重要性,以及多个修饰器的执行顺序。掌握这些概念将使你能够更有效地使用 Solidity 的修饰器功能。

错误处理和安全

Solidity 中有三种主要的错误处理机制:requireassertrevert,以及从 Solidity 0.8.4 版本开始引入的自定义错误(Custom Errors)。它们在用途、Gas 消耗和提供的错误信息方面有所不同。

1. require(bool condition, string memory message)

  • 用途: 用于验证输入条件或外部组件的状态。通常用于检查函数参数是否有效,或者合约的状态是否满足执行条件。

  • 行为: 如果 conditionfalse,则函数执行会回退,所有状态更改都会被撤销,并向调用者返回一个错误信息(可选)。

  • Gas 消耗: 相对较低,比 assert 高,但比 revert(不带错误信息时)稍低。

  • 示例:

    Solidity

    function transfer(address recipient, uint amount) public {
        require(amount > 0, "Amount must be greater than zero.");
        require(balances[msg.sender] >= amount, "Insufficient balance.");
    
        balances[msg.sender] -= amount;
        balances[recipient] += amount;
    }
    

2. assert(bool condition)

  • 用途: 用于检查内部错误和不变量。通常用于确保代码逻辑的正确性,例如检查计算结果是否符合预期。

  • 行为: 如果 conditionfalse,则函数执行会回退,所有状态更改都会被撤销。assert 会导致一个无效的操作码,这意味着它会消耗比 require 更多的 Gas。

  • Gas 消耗: 较高,因为它会产生无效的操作码。

  • 重要区别: assert 主要用于捕捉“绝对不应该发生”的错误。如果 assert 失败,通常意味着合约代码中存在严重的 bug。

  • 示例:

    Solidity

    function sqrt(uint y) internal pure returns (uint z) {
        if (y > 3) {
            z = sqrt(y / 2) * 2;
        } else {
            z = 1;
        }
        require((z * z <= y) && (y < (z + 1) * (z + 1)), "sqrt: internal error"); // 检查结果是否合理
        assert(z * z <= y); // 检查平方根是否小于等于原数,这是一个内部不变量
    }
    

3. revert()revert(string memory reason)

  • 用途: 用于手动触发错误回退。可以用于任何需要中断函数执行并撤销状态更改的情况。

  • 行为: 函数执行会回退,所有状态更改都会被撤销。可以提供一个可选的错误信息。

  • Gas 消耗: 不带错误信息时 Gas 消耗最低,带错误信息时根据信息长度消耗增加。

  • 灵活性:requireassert 更灵活,可以在任何地方使用。

  • 示例:

    Solidity

    function withdraw(uint amount) public {
        if (amount > balances[msg.sender]) {
            revert("Insufficient funds.");
        }
    
        balances[msg.sender] -= amount;
        // ...
    }
    
    function someComplexLogic(uint x) public {
        // ... 一些复杂的逻辑 ...
        if (/* 某个条件不满足 */) {
            revert(); // 不带错误信息的回退
        }
        // ...
    }
    

4. 自定义错误(Custom Errors)(Solidity 0.8.4+)

  • 用途: 提供更详细和 Gas 效率更高的错误信息。

  • 定义: 使用 error 关键字定义错误类型。

  • 使用: 使用 revert 关键字和一个自定义错误实例来触发回退。

  • Gas 消耗: 比使用字符串的 revertrequire 更节省 Gas。

  • 示例:

    Solidity

    error InsufficientBalance(uint available, uint required);
    
    function withdraw(uint amount) public {
        if (amount > balances[msg.sender]) {
            revert InsufficientBalance(balances[msg.sender], amount);
        }
    
        balances[msg.sender] -= amount;
    }
    

总结

方法适用场景优点缺点Gas 消耗
require输入验证、外部条件检查易读,提供错误信息错误信息占用更多 Gas(相对于自定义错误)较低
assert内部错误、不变量检查确保代码逻辑正确性错误信息不明确,Gas 消耗较高较高
revert手动触发回退,任何需要中断执行的情况灵活,可提供错误信息不带信息时最省 Gas,带信息时 Gas 消耗增加不带信息时最低,带信息时根据信息长度增加
自定义错误提供详细错误信息,节省 Gas更具体,节省 Gas需要 Solidity 0.8.4 及以上版本最低

我们自定义错误类型时,一般选择的类型为结构体,因为选择其他类型时我们得到一项信息,但是选择结构体我们可以尽可能多的承载信息

我们在自定义错误时选择error而不选则结构体?

  1. Gas 效率: error 编码方式比字符串和 struct 更高效,因此 Gas 消耗更低。这是最重要的原因。
  2. ABI 编码: error 有专门的 ABI 编码方式,更简洁,更易于解析。
  3. 语义明确: error 关键字明确表明这是用于错误处理的,使代码更易读懂。
  4. 避免歧义: 使用 struct 来表示错误可能会与其他数据结构混淆,而 error 则不会。

什么时候可能(但不推荐)使用 struct

在极少数情况下,如果你需要非常复杂的错误信息结构,或者需要与其他数据结构共享某些类型定义,你 可能 会考虑使用 struct。但即使在这种情况下,也应该优先考虑使用 error,并尽可能将信息简化。

常用的全局变量

全局变量在使用的过程中,无需额外的声明,直接使用即可

Solidity 中有很多常用的全局变量,它们提供了关于区块链、交易和执行环境的重要信息。这些变量主要分为以下几类:

1. 区块和交易属性(Block and Transaction Properties)

  • blockhash(uint blockNumber) returns (bytes32) 给定区块的哈希值。只适用于最近的 256 个区块,不包含当前区块。如果 blockNumber 超出范围,返回 0。
  • block.basefee (uint) 当前区块的基础费用(EIP-1559 引入)。
  • block.chainid (uint) 当前链的 ID。
  • block.coinbase (address) 当前区块矿工的地址。
  • block.difficulty (uint) 当前区块的难度。
  • block.gaslimit (uint) 当前区块的 gas 上限。
  • block.number (uint) 当前区块号。
  • block.timestamp (uint) 自 Unix epoch 起始,当前区块以秒计的时间戳。
  • msg.data (bytes) 完整的 calldata。
  • msg.sender (address) 消息发送者(当前调用者)。
  • msg.sig (bytes4) calldata 的前 4 字节(也就是函数标识符)。
  • msg.value (uint) 随消息发送的 Wei 的数量。
  • tx.gasprice (uint) 交易的 gas 价格。
  • tx.origin (address) 交易的原始发送者(发起交易的外部账户)。

2. 与地址相关的(Address Related)

  • <address>.balance (uint256) 给定地址的 Wei 余额。
  • <address>.code (bytes memory) 给定地址的代码(如果它是合约)。
  • <address>.codehash (bytes32) 给定地址的代码哈希。

3. ABI 编码和解码函数(ABI Encoding and Decoding Functions)

  • abi.decode(bytes memory encodedData, (...)) 解码 ABI 编码的数据。
  • abi.encode(...) returns (bytes memory) ABI 编码给定的参数。
  • abi.encodeWithName(string memory signature, ...) returns (bytes memory) ABI 编码给定的参数,并附加函数签名。
  • abi.encodeWithSignature(bytes4 signature, ...) returns (bytes memory) ABI 编码给定的参数,并附加 4 字节的函数选择器。

4. 错误处理(Error Handling)

  • assert(bool condition) 如果条件为假,则中止执行并回退所有状态更改(用于内部错误)。
  • require(bool condition, string memory message) 如果条件为假,则中止执行并回退所有状态更改,并提供错误信息(用于输入验证)。
  • revert()revert(string memory reason) 中止执行并回退所有状态更改,可以选择提供错误信息。

5. 数学和密码学函数(Mathematical and Cryptographic Functions)

  • addmod(uint x, uint y, uint k) returns (uint) 计算 (x + y) % k,即使 x + y 溢出也不会出错。
  • mulmod(uint x, uint y, uint k) returns (uint) 计算 (x * y) % k,即使 x * y 溢出也不会出错。
  • keccak256(bytes memory) returns (bytes32) 计算输入的 Keccak-256 哈希值。
  • sha256(bytes memory) returns (bytes32) 计算输入的 SHA-256 哈希值。
  • ripemd160(bytes memory) returns (bytes20) 计算输入的 RIPEMD-160 哈希值。
  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address) 从椭圆曲线签名中恢复签名者的地址。

6. 类型信息(Type Information)

  • type(T).name 类型 T 的名称。
  • type(T).creationCode 类型 T 的创建代码(仅适用于合约)。
  • type(T).runtimeCode 类型 T 的运行时代码(仅适用于合约)。

使用示例

Solidity

pragma solidity ^0.8.0;

contract GlobalVariables {
    function getBlockInfo() public view returns (uint, uint, address) {
        return (block.number, block.timestamp, block.coinbase);
    }

    function getMsgInfo() public payable view returns (address, uint) {
        return (msg.sender, msg.value);
    }

    function checkBalance(address _addr) public view returns (uint) {
        return _addr.balance;
    }
}

重要提示

  • 并非所有全局变量都可以在所有上下文中使用。例如,block 相关的变量在 viewpure 函数中是可用的,但在构造函数中有些属性可能不可用。
  • 理解这些全局变量对于编写安全的智能合约至关重要。例如,使用 msg.sender 进行权限控制,使用 block.timestamp 进行时间戳操作等。

继承和交互

继承

如果要继承某个 contract 的话,使用 is 关键词

Copy

contract AddFiveStorage is SimpleStorage

Solidity 支持多重继承:

Copy

contract Ownable {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Not the owner");
        _;
    }
}

contract Pausable {
    bool public paused;

    modifier whenNotPaused() {
        require(!paused, "Contract is paused");
        _;
    }
}

contract MyContract is Ownable, Pausable {
    function doSomething() public onlyOwner whenNotPaused {
        // 函数逻辑
    }
}

如果想修改继承过来的合约里的函数,则需要用 override 关键词,并且父级合约中的函数需要带上 virtual 关键词,没有 virtual 的函数都无法被重写

Copy

// 函数中有 virtual 修饰符才能被继承改写
function store(uint256 _favoriteNumber) public virtual  {
	myFavoriteNumber = _favoriteNumber;
}

// 要改写继承过来的函数,需要带上 override 关键词
function store(uint256 _newNumber) public override {
    myFavoriteNumber = _newNumber + 5;
}

接口和抽象合约

在Solidity中,接口(Interface)和抽象合约(Abstract Contract)都是用于定义合约的蓝图或规范的重要工具,它们都允许你定义一组函数签名,而无需提供具体的实现。它们的主要区别在于其用途和限制。

1. 接口(Interface)

接口类似于现实世界中的接口,它定义了一组合约必须实现的方法,但不包含任何状态变量或具体的实现代码。你可以把它看作是一份“合同”,任何实现了该接口的合约都必须遵守这份“合同”中规定的方法。

特点:

  • 纯声明: 接口只包含函数声明(函数名、参数类型、返回类型),没有函数体(实现代码)。
  • 无状态变量: 接口不能声明任何状态变量。
  • 无构造函数: 接口不能定义构造函数。
  • 不能继承合约: 接口不能继承其他合约,但可以继承其他接口。
  • 所有函数默认为 external 接口中的所有函数声明都隐式地被声明为 external,这意味着它们只能从合约外部调用。
  • 用途: 主要用于定义合约之间的交互标准,提高代码的互操作性和模块化程度。

示例:

Solidity

interface MyInterface {
    function getValue() external view returns (uint);
    function setValue(uint _newValue) external;
}

contract MyContract implements MyInterface {
    uint public myValue;

    function getValue() external view override returns (uint) {
        return myValue;
    }

    function setValue(uint _newValue) external override {
        myValue = _newValue;
    }
}

在这个例子中,MyInterface 定义了两个函数 getValuesetValueMyContract 实现了 MyInterface 接口,因此它必须提供这两个函数的具体实现。

2. 抽象合约(Abstract Contract)

抽象合约是一种不能直接部署的合约。它至少包含一个未实现的函数(即只有函数签名而没有函数体的函数),这些未实现的函数需要使用 virtual 关键字声明。其他合约可以通过继承抽象合约并提供这些未实现函数的具体实现来使用抽象合约的功能。

特点:

  • 可以包含部分实现: 抽象合约可以包含已实现的函数以及未实现的函数。
  • 可以有状态变量: 抽象合约可以声明状态变量。
  • 可以有构造函数: 抽象合约可以定义构造函数。
  • 可以继承其他合约: 抽象合约可以继承其他合约。
  • 未实现的函数需要 virtual 关键字: 子合约需要使用 override 关键字来重写这些函数。
  • 用途: 主要用于定义合约的通用模板,提供一些默认实现,并强制子合约实现特定的功能。

示例:

Solidity

abstract contract MyAbstractContract {
    uint public myValue;

    constructor(uint _initialValue) {
        myValue = _initialValue;
    }

    function getValue() public view virtual returns (uint); // 未实现函数
    function setValue(uint _newValue) public virtual;      // 未实现函数

    function doubleValue() public { // 已实现函数
        myValue *= 2;
    }
}

contract MyContract is MyAbstractContract {
    constructor(uint _initialValue) MyAbstractContract(_initialValue) {}

    function getValue() public view override returns (uint) {
        return myValue;
    }

    function setValue(uint _newValue) public override {
        myValue = _newValue;
    }
}

在这个例子中,MyAbstractContract 是一个抽象合约,它定义了 getValuesetValue 两个未实现的函数,以及 doubleValue 一个已实现的函数。MyContract 继承了 MyAbstractContract,并提供了 getValuesetValue 的具体实现。

主要区别总结

特性接口(Interface)抽象合约(Abstract Contract)
实现只能包含函数声明,没有实现可以包含已实现和未实现的函数
状态变量不能包含状态变量可以包含状态变量
构造函数不能有构造函数可以有构造函数
继承只能继承其他接口可以继承其他合约
函数可见性所有函数默认为 external可以使用任何可见性(publicinternalexternalprivate
主要互操作性定义合约的通用模板,提供部分实现,强制子合约实现特定功能
是否可部署不可部署不可部署

选择使用哪个?

  • 如果你只需要定义一组合约需要实现的方法,而不需要提供任何实现或状态变量,那么应该使用接口
  • 如果你需要提供一些默认实现或状态变量,并强制子合约实现特定的功能,那么应该使用抽象合约

通常,接口用于定义更高级别的抽象和交互标准,而抽象合约用于构建更具体的合约模板。理解它们之间的区别可以帮助你更好地设计和组织你的Solidity代码。