惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

G
GRAHAM CLULEY
V
V2EX
WordPress大学
WordPress大学
博客园 - Franky
Last Week in AI
Last Week in AI
博客园 - 司徒正美
有赞技术团队
有赞技术团队
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 【当耐特】
V
Visual Studio Blog
C
CERT Recently Published Vulnerability Notes
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Jina AI
Jina AI
Attack and Defense Labs
Attack and Defense Labs
腾讯CDC
The Hacker News
The Hacker News
Hugging Face - Blog
Hugging Face - Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
J
Java Code Geeks
人人都是产品经理
人人都是产品经理
阮一峰的网络日志
阮一峰的网络日志
T
Tailwind CSS Blog
S
SegmentFault 最新的问题
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
A
Arctic Wolf
量子位
博客园 - 聂微东
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
N
News and Events Feed by Topic
雷峰网
雷峰网
博客园_首页
Google Online Security Blog
Google Online Security Blog
Spread Privacy
Spread Privacy
罗磊的独立博客
H
Hacker News: Front Page
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
TaoSecurity Blog
TaoSecurity Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
博客园 - 三生石上(FineUI控件)
宝玉的分享
宝玉的分享
IT之家
IT之家
The Cloudflare Blog
爱范儿
爱范儿
博客园 - 叶小钗
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Apple Machine Learning Research
Apple Machine Learning Research
酷 壳 – CoolShell
酷 壳 – CoolShell

pseudoyu

周报 #106 - OpenClaw 实践、搬家与生活的能量 周报 #105 - 打破无聊的社会 2025 年末回顾 - Anything different is good 周报 #104 - 被遗忘的生活一隅 周报 #103 - 工作随想与被吞噬的生活 周报 #102 - 我是如何使用 AI 的 周报 #101 - 消失了三个月,我的生活发生了什么变化 周报 #100 - 过去这十年 周报 #99 - 离开 RSS3,迷茫与重启人生 周报 #98 - 生活之书与 Agentic Coding 周报 #97 - 日本旅行小记 28 岁的自白:去找寻自己 周报 #96 - 基于 Roo Code/Cursor 的 iOS 开发工作流 周报 #95 - All AI 与 No AI 周报 #94 - 生活的界限与真实感 周报 #93 - 婚礼纪录片与失落的电影院 周报 #92 - 数码产品 Porn 与断舍离 周报 #91 - 一席、消失的时代与自由意志 周报 #90 - OVH 服务器、Coolify 与对 AI Coding 的思考 周报 #89 - 博客改版与 TheWan App 发布 周报 #88 - 方大同、《回留》与人生清单 周报 #87 - 我们主持了自己的宝可梦婚礼,还为它办了一个展 周报 #86 - 用影像打败时间 周报 #85 - 京华烟云 Moment in Peking 周报 #84 - 想起他们 Echos of Reverie 周报 #83 - 年初收纳(房间、设备、服务、软件) 周报 #82 - Brand New Year 与写日记 2024 年末回顾 - 不完美的日子 周报 #81 - Engaged -> Married 与时间的尺度 周报 #80 - 关于求婚、爱情与婚姻 求婚誓言 | 爱的本质是被看见 周报 #79 - 清迈旅居、曼谷之行与 Follow RSSHub 开发实践 #01:Telegram 频道 RSS 订阅实现与部署方案 Follow —— 信息获取的另一种形态 周报 #78 - NAS、Chromebook 与 Zeabur 折腾小记 周报 #77 - 一间只属于自己的房间(对亲密关系的思考) 周报 #76 - 关于 Adulting 的思考 周报 #75 - 回答 Dayu 关于独立博客的 9 个问题 周报 #74 - 清迈异闻录 周报 #73 - 《社交网络》、Ego 与中秋的现充生活 周报 #71 - Indie App 体验计划 周报 #70 - 消失的附近,Burnout 与 Boreout 周报 #69 - AI Coding 体验与学习的源动力 周报 #68 - 关于写博客这件事与我生活的色彩 使用 GoatCounter 与 Zeabur 搭建网站数据统计系统 周报 #67 - 使用 follow 重塑我的信息输入系统 周报 #66 - 10x 工程师、技术热情与个人工具箱 从零开始搭建你的免费博客评论系统(Remark42 + fly.io) 周报 #65 - Adventure X 体验、Apple Notes 笔记实践与 EpubKit 周报 #64 - 生活在此处(《斯通纳》与《Normal People》) 使用 WebP Cloud 与 Cloudflare WAF 为你的图床添加隐私和版权保护 当云服务器崩溃时,我是如何救援重要数据的 周报 #63 - 不愉快的订花经历、商家和消费者与日渐 AI 化的人 从零开始搭建你的免费图床系统(Cloudflare R2 + WebP Cloud) 2024 年了,我的博客有了什么变化 周报 #62 - 香港之行、5am club 计划与 Rust 学习 周报 #61 - 好朋友的婚礼与对婚姻的思考 27 岁的自白:成长是一件很扫兴的事 周报 #60 - 虫子旁、教育理念与 EpubKit 周报 #59 - 沙漠之行、家庭与家人 周报 #58 - 远程工作这一年 周报 #57 - 生活在别处(巴厘岛之行) 周报 #56 - 面基 Randy 与考科目二 周报 #55 - 油画体验、博客系统升级与对 self-hosting 的思考 周报 #54 - 漂流计划、钱包被盗与 Home Server 周报 #53 - 空白的三个月、生活漂流与春节 周报 #52 - 自谦型人格与感情的另一个视角 周报 #51 - 追星小鹿 Lawrence 与生病 周报 #50 - 天坛漫步、重拾阅读和费曼学习法 周报 #49 - 烧焦的键盘、庞贝神话展、健康调整与神奇的梦 周报 #48 - 内蒙之行、清迈游民、重启的周报与生活 周报 #47 - 采访、拳击与工作状态 周报 #46 - 告别长发、周报初心与合约开发 个人信息获取与知识管理系统(Heptabase + Logseq + Readwise) 周报 #45 - 读书笔记、知识管理与双城生活 周报 #44 - 静寂工人、xLog 与日常 周报 #43 - In the Mood for Love 周报 #42 - 感情观、生活状态与自我 周报 #41 - 生日的仪式感、时间管理与北京之行 26 岁的自白:推石头的人 周报 #40 - 开源之夏、代码之外与流逝的睡眠 周报 #39 - 初患新冠、过去的心绪与失而复得的分享欲 周报 #38 - Foundry、Logseq 与 Surge Ponte 周报 #37 - 浮生半月闲( 武汉 | 香港 | 五月天) 周报 #36 - 肠胃炎、捏捏近况与新的旅途 周报 #35 - 登报、搬家和生病的捏捏 周报 #34 - Don't Stop the Clocks 周报 #33 - 北京 x 故事 x 离别 周报 #32 - 武汉、久别重逢与流动的心绪 周报 #31 - 开源、前端开发与 ChatGPT 实践 周报 #30 - 开源预算、写作初心与对技术的谦卑 Cosmos 区块链架构与 Tendermint 共识机制 周报 #29 - 日语学习经验、Steam Deck 与居酒屋 周报 #28 - Mastodon、故乡与捉迷藏的猫 周报 #27 - 何以为家 周报 #26 - 博客、客制化键盘和新服务器 周报 #25 - 基于 Crossbell 的个人信息输出与同步系统(重构) 摄影集 #02 - Moment In Peking (The Autumn) 摄影集 #01 - Cat In The Box 2022 年末回顾 - 迷茫、低谷与改变
Solidity 智能合约开发 - 基础
pseudoyu · 2022-05-24 · via pseudoyu

前言

去年读研的时候上的 HKU 的 <COMP7408 Distributed Ledger and Blockchain Technology>,课程中学习了以太坊智能合约的开发,做了一个简单的图书管理 ÐApp,然后毕业设计也选择了基于 Ethereum 做了一个音乐版权应用,详见 Uright - 区块链音乐版权管理ÐApp,对 Solidity 开发有一些基础了解。

后来工作后主要做联盟链和业务开发这一块,很久没有碰过合约,对于语法和底层一些概念都已经一知半解,正好最近做的项目是基于 EVM 的一条链,涉及了一些基本的存证、回检和迁移相关合约的开发,调试起来有些吃力,于是打算系统学习一下,梳理一下笔记成文章,敦促自己好好思考总结。

这系列文章也会收录在我的个人知识库项目 《区块链入门指南》中,希望在学习过程中不断完善。有兴趣的朋友也可以访问项目仓库参与贡献或提出建议。

本文为系列第一篇,主要涉及 Solidity 基础知识。

智能合约 与 Solidity 语言

智能合约是运行在链上的程序,合约开发者可以通过智能合约实现与链上资产/数据进行交互,用户可以通过自己的链上账户来调用合约,访问资产与数据。因为区块链保留区块历史记录的链式结构、去中心化、不可篡改等特征,智能合约相比传统应用来说能更公正、透明。

然而,因为智能合约需要与链进行交互,部署、数据写入等操作都会消耗一定费用,数据存储与变更成本也比较高,因此在设计合约时需要着重考虑资源的消耗。此外,常规智能合约一经部署就无法进行修改,因此,合约设计时也需要多考虑其安全性、可升级性与拓展性。

Solidity 是一门面向合约的、为实现智能合约而创建的高级编程语言,在 EVM 虚拟机上运行,语法整体类似于 Javascript,是目前最流行的智能合约语言,也是入门区块链与 Web3 所必须掌握的语言。针对上述的一些合约编写的问题,Solidity 也都有相对完善的解决方案支持,后续会详细讲解。

开发/调试工具

与常规编程语言不同,Solidity 智能合约的开发往往无法直接通过一个 IDE 或本地环境进行方便的调试,而是需要与一个链上节点进行交互。开发调试往往也不会直接与主网(即真实资产、数据与业务所在的链)进行交互,否则需要承担高额手续费。目前开发调试主要有以下几种方式与框架:

  1. Truffle。Truffle 是一个非常流行的 Javascript 的 Solidity 合约开发框架,提供了完整的开发、测试、调试工具链,可以与本地或远程网络进行交互。
  2. Brownie。Brownie 是一个基于 Python 的 Solidity 合约开发框架,以简洁的 Python 语法为调试和测试提供了便捷的工具链。
  3. Hardhat。Hardhat 是另一个基于 Javascript 的开发框架,提供了非常丰富的插件系统,适合开发复杂的合约项目。

除了开发框架外,更好地进行 Solidity 还需要熟悉一些工具:

  1. Remix IDE。通过 Ethereum 官方提供的基于浏览器的 Remix 开发工具进行调试,Remix 会提供完整的 IDE、编译工具、部署调试的测试节点环境、账户等,可以很方便地进行测试,这是我学习使用时用的最多的工具。Remix 还可以通过 MetaMask 插件与测试网、主网进行直接交互,部分生产环境也会使用它进行编译部署。
  2. Remix IDE 对于语法提示等并不完善,因此,可以使用 Visual Studio Code 配合 Solidity 进行编写,有更好的体验。
  3. MetaMask。一个常用的钱包应用,开发过程中可以通过浏览器插件与测试网、主网进行交互,方便开发者进行调试。
  4. Ganache。Ganache 是一个开源的虚拟本地节点,提供了一个虚拟链网络,可以通过各类 Web3.js、Remix 或一些框架工具与之交互,适合有一定规模的项目进行本地调试与测试。
  5. Infura。Infura 是一个 IaaS(Infrastructure as a Service)产品,我们可以申请自己的 Ethereum 节点,通过 Infura 提供的 API 进行交互,可以很方便地进行调试,也更接近生产环境。
  6. OpenZeppelin。OpenZeppelin 提供了非常多的合约开发库与应用,能兼顾安全、稳定的同时给予开发者更好的开发体验,降低合约开发成本。

合约编译/部署

Solidity 合约是以 .sol 为后缀的文件,无法直接执行,需要编译为 EVM(Ethereum Virtual Machine)可识别的字节码才能在链上运行。

compile_solidity

编译完成后,由合约账户进行部署到链上,其他账户可通过钱包与合约进行交互,实现链上业务逻辑。

核心语法

经过上文,我们对 Solidity 的开发、调试与部署有了一定了解。接下来我们就具体学习一下 Solidity 的核心语法。

数据类型

与我们常见的编程语言类似,Solidity 有一些内置数据类型。

基本数据类型

  • boolean,布尔类型有 truefalse 两种类型,可以通过 bool public boo = true; 来定义,默认值为 false
  • int,整数类型,可以指定 int8int256,默认为 int256,通过 int public int = 0; 来定义,默认值为 0,还可以通过 type(int).mintype(int).max 来查看类型最小和最大值
  • uint,非负整数类型,可以指定 uint8uint16uint256,默认为 uint256,通过 uint8 public u8 = 1; 来定义,默认值为 0
  • address,地址类型,可以通过 address public addr = 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c; 来定义,默认值为 0x0000000000000000000000000000000000000000
  • bytesbyte[] 的缩写,分为固定大小数组和可变数组,通过 bytes1 a = 0xb5; 来定义

还有一些相对复杂的数据类型,我们单独进行讲解。

Enum

Enum 是枚举类型,可以通过以下语法来定义

enum Status {
    Unknown,
    Start,
    End,
    Pause
}

并通过以下语法来进行更新与初始化


// 实例化枚举类型
Status public status;

// 更新枚举值
function pause() public {
    status = Status.Pause;
}

// 初始化枚举值
function reset() public {
    delete status;
}

数组

数组是一种存储同类元素的有序集合,通过 uint[] public arr; 来进行定义,在定义时可以预先指定数组大小,如 uint[10] public myFixedSizeArr;

需要注意的是,我们可以在内存中创建数组(关于 memorystorage 等差异后续会详细讲解),但是必须固定大小,如 uint[] memory a = new uint[](5);

数组类型有一些基本操作方法,如下:

// 定义数组类型
uint[7] public arr;

// 添加数据
arr.push(7);

// 删除最后一个数据
arr.pop();

// 删除某个索引值数据
delete arr[1];

// 获取数组长度
uint len = arr.length;

mapping

mapping 是一种映射类型,使用 mapping(keyType => valueType) 来定义,其中键需要是内置类型,如 bytesintstring 或合约类型,而值可以是任何类型,如嵌套 mapping 类型。需要注意的是,mapping 类型是不能被迭代遍历的,需要遍历则需要自行实现对应索引。

下面说明一下各类操作:

// 定义嵌套 mapping 类型
mapping(string => mapping(string => string)) nestedMap;

// 设置值
nestedMap[id][key] = "0707";

// 读取值
string value = nestedMap[id][key];

// 删除值
delete nestedMap[id][key];

Struct

struct 是结构类型,对于复杂业务,我们经常需要定义自己的结构,将关联的数据组合起来,可以在合约内进行定义

contract Struct {
    struct Data {
        string id;
        string hash;
    }

    Data public data;

    // 添加数据
    function create(string calldata _id) public {
        data = Data{id: _id, hash: "111222"};
    }

    // 更新数据
    function update(string _id) public {
        // 查询数据
        string id = data.id;

        // 更新
        data.hash = "222333"
    }
}

也可以单独文件定义所有需要的结构类型,由合约按需导入

// 'StructDeclaration.sol'

struct Data {
    string id;
    string hash;
}
// 'Struct.sol'

import "./StructDeclaration.sol"

contract Struct {
    Data public data;
}

变量/常量/Immutable

变量是 Solidity 中可改变值的一种数据结构,分为以下三种:

  • local 变量
  • state 变量
  • global 变量

其中, local 变量定义在方法中,而不会存储在链上,如 string var = "Hello";;而 state 变量在方法之外定义,会存储在链上,通过 string public var; 定义变量,写入值时会发送交易,而读取值则不会;global 变量则是提供了链信息的全局变量,如当前区块时间戳变量,uint timestamp = block.timestamp;,合约调用者地址变量,address sender = msg.sender; 等。

变量可以通过不同关键字进行声明,表示不同的存储位置。

  • storage,会存储在链上
  • memory,在内存中,只有方法被调用的时候才存在
  • calldata,作为调用方法传入参数时存在

而常量是一种不可以改变值的变量,使用常量可以节约 gas 费用,我们可以通过 string public constant MY_CONSTANT = "0707"; 来进行定义。immutable 则是一种特殊的类型,它的值可以在 constructor 中初始化,但不可以再次改变。灵活使用这几种类型可以有效节省 gas 费并保障数据安全。

函数

在 Solidity 中,函数用来定义一些特定业务逻辑。

权限声明

函数分为不同的可见性,用户不同的关键字进行声明:

  • public,任何合约都可调用
  • private,只有定义了该方法的合约内部可调用
  • internal,只有在继承合约可调用
  • external,只有其他合约和账户可调用

查询数据的合约函数也有不同的声明方式:

  • view 可以读取变量,但不能更改
  • pure 不可以读也不可以修改

函数修饰符

modifier 函数修饰符可以在函数运行前/后被调用,主要用来进行权限控制、对输入参数进行校验以及防止重入攻击等。这三种功能修饰符可以通过以下语法定义:

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

modifier validAddress(address _addr) {
    require(_addr != address(0), "Not valid address");
    _;
}

modifier noReentrancy() {
    require(!locked, "No reentrancy");
    locked = true;
    _;
    locked = false;
}

使用函数修饰符则是需要在函数声明时添加对应修饰符,如:

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

function decrement(uint i) public noReentrancy {
    x -= i;

    if (i > 1) {
        decrement(i - 1);
    }
}

函数选择器

当函数被调用时,calldata 的前四个字节要指定以确认调用哪个函数,被称为函数选择器。

addr.call(abi.encodeWithSignature("transfer(address,uint256)", 0xSomeAddress, 123))

上述代码 abi.encodeWithSignature() 返回值的前四个字节就是函数选择器。我们如果在执行前预先计算函数选择器的话可以节约一些 gas 费。

contract FunctionSelector {
    function getSelector(string calldata _func) external pure returns (bytes4) {
        return bytes4(keccak256(bytes(_func)));
    }
}

条件/循环结构

条件

Solidity 使用 ifelse ifelse 关键字来实现条件逻辑:

if (x < 10) {
    return 0;
} else if (x < 20) {
    return 1;
} else {
    return 2;
}

也可以使用简写形式:

x < 20 ? 1 : 2;

循环

Solidity 使用 forwhiledo while 关键字来实现循环逻辑,但是因为后两者容易达到 gas limit 边界值,所以基本上不用。

for (uint i = 0; i < 10; i++) {
    // 业务逻辑
}
uint j;
while (j < 10) {
    j++;
}

合约

构造器

Solidity 的 constructor 可以在创建合约的时候执行,主要用来初始化

constructor(string memory _name) {
    name = _name;
}

如果合约之间存在继承关系,constructor 也会按照继承顺序。

接口

Interface,通过声明接口来进行合约交互,有以下要求:

  • 不能实现任何方法
  • 可以继承其他接口
  • 所有方法都必须声明为 external
  • 不能声明构造方法
  • 不能声明状态变量

接口用如下语法进行定义:

contract Counter {
    uint public count;

    function increment() external {
        count += 1;
    }
}

interface ICounter {
    function count() external view returns (uint);
    function increment() external;
}

调用则是通过

contract MyContract {
    function incrementCounter(address _counter) external {
        ICounter(_counter).increment();
    }

    function getCount(address _counter) external view returns (uint) {
        return ICounter(_counter).count();
    }
}

继承

Solidity 合约支持继承,且可以同时继承多个,使用 is 关键字。

函数可以进行重写,需要被继承的合约方法需要声明为 virtual,重写方法需要使用 override 关键字。

// 定义父合约 A
contract A {
    function foo() public pure virtual returns (string memory) {
        return "A";
    }
}

// B 合约继承 A 合约并重写函数
contract B is A {
    function foo() public pure virtual override returns (string memory) {
        return "B";
    }
}

// D 合约继承 B、C 合约并重写函数
contract D is B, C {
    function foo() public pure override(B, C) returns (string memory) {
        return super.foo();
    }
}

有几点需要注意的是,继承顺序会影响业务逻辑,state 状态变量是不可以被继承的。

如果子合约想调用父合约,除了直接调用外,还可以通过 super 关键字来调用,如下:

contract B is A {
    function foo() public virtual override {
        // 直接调用
        A.foo();
    }

    function bar() public virtual override {
        // 通过 super 关键字调用
        super.bar();
    }
}

合约创建

Solidity 中可以从另一个合约中使用 new 关键字来创建另一个合约

function create(address _owner, string memory _model) public {
    Car car = new Car(_owner, _model);
    cars.push(car);
}

solidity 0.8.0 后支持 create2 特性创建合约

function create2(address _owner, string memory _model, bytes32 _salt) public {
    Car car = (new Car){salt: _salt}(_owner, _model);
    cars.push(car);
}

导入合约/外部库

复杂业务中,我们往往需要多个合约之间进行配合,这时候可以使用 import 关键字来导入合约,分为本地导入 import "./Foo.sol"; 与外部导入 import "https://github.com/owner/repo/blob/branch/path/to/Contract.sol"; 两种方式。

外部库和合约类似,但不能声明状态变量,也不能发送资产。如果库的所有方法都是 internal 的话会被嵌入合约,如果非 internal,需要提前部署库并且链接起来。

library SafeMath {
    function add(uint x, uint y) internal pure returns (uint) {
        uint z = x + y;
        require(z >= x, "uint overflow");
        return z;
    }
}
contract TestSafeMath {
    using SafeMath for uint;
}

事件

事件机制是合约中非常重要的一个设计。事件允许将信息记录到区块链上,DApp 等应用可以通过监听事件数据来实现业务逻辑,存储成本很低。以下是一个简单的日志抛出机制:

// 定义事件
event Log(address indexed sender, string message);
event AnotherLog();

// 抛出事件
emit Log(msg.sender, "Hello World!");
emit Log(msg.sender, "Hello EVM!");
emit AnotherLog();

定义事件时可以传入 indexed 属性,但最多三个,加了后可以对这个属性的参数进行过滤,var event = myContract.transfer({value: ["99","100","101"]});

错误处理

链上错误处理也是合约编写的重要环节。Solidity 可以通过以下几种方式抛出错误。

require 都是在执行前验证条件,不满足则抛出异常。

function testRequire(uint _i) public pure {
    require(_i > 10, "Input must be greater than 10");
}

revert 用来标记错误与进行回滚。

function testRevert(uint _i) public pure {
    if (_i <= 10) {
        revert("Input must be greater than 10");
    }
}

assert 要求一定要满足条件。

function testAssert() public view {
    assert(num == 0);
}

注意,在 Solidity 中,当出现错误时会回滚交易中发生的所有状态改变,包括所有的资产,账户,合约等。

try / catch 也可以捕捉错误,但只能捕捉来自外部函数调用和合约创建的错误。

event Log(string message);
event LogBytes(bytes data);

function tryCatchNewContract(address _owner) public {
    try new Foo(_owner) returns (Foo foo) {
        emit Log("Foo created");
    } catch Error(string memory reason) {
        emit Log(reason);
    } catch (bytes memory reason) {
        emit LogBytes(reason);
    }
}

payable 关键字

我们可以通过声明 payable 关键字设置方法可从合约中接收 ether

// 地址类型可以声明 payable
address payable public owner;

constructor() payable {
    owner = payable(msg.sender);
}

// 方法声明 payable 来接收 Ether
function deposit() public payable {}

Ether 交互

Ether 交互是智能合约的重要应用场景,主要分为发送和接收两部分,分别有不同的方法实现。

发送

主要通过 transfersendcall 方法实现,其中 call 优化了对重入攻击的防范,在实际应用场景中建议使用(但一般不用来调用其他函数)。

contract SendEther {
  function sendViaCall(address payable _to) public payable {
    (bool sent, bytes memory data) = _to.call{value: msg.value}("");
    require(sent, "Failed to send Ether");
  }
}

而如果需要调用另一个函数,则一般使用 delegatecall

contract B {
    uint public num;
    address public sender;
    uint public value;

    function setVars(uint _num) public payable {
        num = _num;
        sender = msg.sender;
        value = msg.value;
    }
}

contract A {
    uint public num;
    address public sender;
    uint public value;

    function setVars(address _contract, uint _num) public payable {
        (bool success, bytes memory data) = _contract.delegatecall(
            abi.encodeWithSignature("setVars(uint256)", _num)
        );
    }
}

接收

接收 Ether 主要用 receive() external payablefallback() external payable 两种。

当一个不接受任何参数也不返回任何参数的函数、当 Ether 被发送至某个合约但 receive() 方法未实现或 msg.data 非空时,会调用 fallback() 方法。

contract ReceiveEther {
    // 当 msg.data 为空时
    receive() external payable {}

    // 当 msg.data 非空时
    fallback() external payable {}

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

Gas 费

在 EVM 中执行交易需要耗费 gas 费,gas spent 表示需要多少 gas 量,gas price 为 gas 的单位价格,EtherWei 是价格单位,1 ether == 1e18 wei。

合约会对 Gas 进行限制,gas limit 由发起交易的用户设置,最多花多少 gas,block gas limit,由区块链网络决定,这个区块中最多允许多少 gas。

我们在合约开发中要尤其考虑尽量节约 gas 费,有以下几个常用技巧:

  1. 使用 calldata 来替换 memory
  2. 将状态变量载入内存
  3. 使用 i++ 而不是 ++i
  4. 缓存数组元素
function sumIfEvenAndLessThan99(uint[] calldata nums) external {
    uint _total = total;
    uint len = nums.length;

    for (uint i = 0; i < len; ++i) {
        uint num = nums[i];
        if (num % 2 == 0 && num < 99) {
            _total += num;
        }
    }

    total = _total;
}

总结

以上就是我们系列第一篇,Solidity 基础知识,后续文章会对其常见应用和实用编码技巧进行学习总结,欢迎大家持续关注。

参考资料

  1. Solidity by Example
  2. Ethereum 區塊鏈!智能合約(Smart Contract)與分散式網頁應用(dApp)入門
  3. 区块链入门指南
  4. Uright - 区块链音乐版权管理ÐApp