university.alchemy4

Calldata

当我们想与智能合约进行通信时,我们从外部拥有账户(Externally Owned Account,EOA)发送一个交易。在这个交易中,有一个数据属性,通常被称为“calldata”。这种调用数据格式对于调用 Solidity 函数来说,无论是来自 EOA 的交易,还是一个合约之间的消息调用,都是相同的。

这个格式大致如下。假设你想调用一个 approve 方法,接受一个 uint 参数:

1
function approve(uint val) external;

我们可以通过对该函数的签名进行哈希处理(使用 keccak256),然后取哈希值的前 4 个字节来定位这个函数。对于 approve 方法来说,哈希值是 keccak256("approve(uint256)")。该哈希值的前 4 个字节是 0xb759f954。这就是我们的 calldata 的第一部分!

接下来,我们需要确定我们要批准多少数量。假设我们要批准的值是 15,那么它的十六进制表示是 0xf。我们需要将这个值填充为 256 位,或者说 64 个十六进制字符。结果的值将是:

1
000000000000000000000000000000000000000000000000000000000000000f

如果我们将这个值与函数签名结合起来,我们的 calldata 将是:

1
0xb759f954000000000000000000000000000000000000000000000000000000000000000f

无论是来自 EOA 的交易,还是合约之间的消息调用,这都会是我们发送 15 给 approve 函数的 calldata。

准备好开始处理 calldata 了吗?让我们开始吧!

接口 (Interfaces)

使一个合约与另一个合约交互的最简单方式是定义目标合约。接口就是为了这个目的而存在的。例如:

1
2
3
interface IToken {
function getBalance(address user) external;
}

我们可以使用这个接口与实现了 getBalance 方法的代币合约进行正确的通信:

1
2
3
// tokenAddress: 我们想要与之通信的合约地址
// userAddress: 我们想要查询余额的地址
uint balance = IToken(tokenAddress).getBalance(userAddress);

在幕后,Solidity 正在创建一个消息调用,该调用为 getBalance 调用编码了 calldata。

你的目标:警告英雄

使用 IHero 接口和传入 sendAlert 的英雄地址,从 Sidekick 合约中向英雄发送警报。

答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IHero {
function alert() external;
}

contract Sidekick {
function sendAlert(address hero) external {
// TODO: alert the hero using the IHero interface
IHero(hero).alert();
}
}

为什么Ihero接口可以传入hero地址,那能不能传入balance余额

要理解这个问题,核心是分清接口的作用数据类型的本质IHero 接口是用来和「实现该接口的合约地址」交互的,而 balance 是地址的属性值(不是合约地址本身),二者不能混为一谈。下面分两部分详细解释:

一、为什么 IHero 接口可以传入 hero 地址?

IHero(hero) 本质是「类型转换」,不是「传入参数」,核心逻辑是:

  1. 接口的本质IHero 接口是一个「函数签名规范」,它定义了“只要某个合约实现了 alert() 函数,就可以被当作 IHero 类型来调用”。
  2. 地址的作用hero 是一个 address 类型的值,代表一个合约的部署地址(这个合约恰好实现了 IHero 接口的 alert() 函数)。
  3. 类型转换的意义IHero(hero) 告诉 Solidity 编译器:“把这个 hero 地址对应的合约,当作实现了 IHero 接口的合约来处理,我要调用它的 alert() 函数”。

简单说:接口是「操作规范」,地址是「操作目标」,接口 + 地址 = 明确“对哪个合约执行哪些规范内的操作”。

二、能不能把 balance 余额传入 IHero 接口?—— 不能(核心原因)

balanceaddress 类型的只读属性(比如 address(this).balancehero.balance),它的类型是 uint256(数字),而 IHero 接口只能和 address 类型(合约地址)绑定,二者的本质和用途完全不同:

特征 hero 地址(address balance 余额(uint256
数据类型 地址类型(标识合约/账户的身份) 无符号整数(纯数值)
作用 定位要交互的合约/账户 表示地址下的ETH余额
能否绑定到 IHero 能(接口需和地址绑定才能调用函数) 不能(接口只认地址,不认数字)

错误示例(直观理解):

如果强行把 balance 传给 IHero,编译器会直接报错:

1
2
3
4
5
6
function sendAlert(address hero) external {
// 获取hero地址的余额(uint256类型)
uint256 heroBalance = hero.balance;
// 错误!IHero() 要求传入address类型,但heroBalance是uint256
IHero(heroBalance).alert();
}

报错原因:IHero() 括号内必须传入 address 类型的值,而 balanceuint256,类型不匹配,编译器无法将一个数字解析为合约地址。

三、如果想操作 balance 余额,该怎么做?

虽然不能把 balance 传入 IHero 接口,但可以基于 hero 地址获取/操作其余额,比如:

  1. 读取 hero 地址的余额:直接通过 address 类型的 balance 属性获取(无需接口);
  2. hero 地址转账(修改余额):将地址转为 payable 后调用 transfer/send/call(也无需 IHero 接口)。

正确示例(结合接口+余额操作):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IHero {
function alert() external;
}

contract Sidekick {
function sendAlertAndCheckBalance(address hero) external {
// 1. 先通过IHero接口调用hero合约的alert()函数(核心需求)
IHero(hero).alert();

// 2. 再获取hero地址的余额(uint256类型)
uint256 heroBalance = hero.balance;
// 比如:打印余额(测试用)
emit HeroBalance(hero, heroBalance);

// 3. 若需向hero地址转账(修改余额),需转为payable
if (heroBalance < 1 ether) {
payable(hero).transfer(1 ether); // 给hero转1ETH
}
}

// 事件:用于打印hero的余额(方便测试)
event HeroBalance(address indexed hero, uint256 balance);
}

四、总结

  1. IHero 接口的核心是「调用指定地址合约的 alert() 函数」,因此必须传入 address 类型的 hero 合约地址;
  2. balance 是地址的数值属性uint256),不是地址本身,无法传入 IHero() 进行接口绑定;
  3. 操作余额的正确方式:基于 hero 地址(address)直接读取/修改,和 IHero 接口是“并行操作”,而非“传入关系”。

简单记:接口绑「地址」,余额是地址的「属性值」,二者是“对象”和“对象的属性”的关系,不能互相替代。

Your Goal: Alert Hero, Manually 你的目标:手动提醒英雄

Alert the Hero, manually this time!
这次手动通知英雄!

Fill in the function signature for the Hero’s alert function. Notice that we are taking the first 4 bytes of the hash of this function and passing it in as calldata to the hero.
填写英雄的alert函数的函数签名。注意,我们要取这个函数哈希值的前4个字节,并将其作为calldata传递给英雄。

答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Sidekick {
function sendAlert(address hero) external {
// TODO: fill in the function signature in the ""
bytes4 signature = bytes4(keccak256("alert()"));

(bool success, ) = hero.call(abi.encodePacked(signature));

require(success);
}
}

Your Goal: Alert the Hero with Arguments

你的目标:用论据提醒英雄
Alert the Hero by calling alert and passing the number of enemies and whether or not they are armed
通过调用alert并传入enemies的数量以及他们是否armed,来向Hero发出警报。

答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Sidekick {
// sendAlert 函数根据传入的参数生成 calldata
function sendAlert(address hero, uint enemies, bool armed) external {
// 使用传入的 enemies 和 armed 参数构造 calldata
bytes memory payload = abi.encodeWithSignature("alert(uint256,bool)", enemies, armed);

// 调用 hero 合约的 alert 函数
(bool success, ) = hero.call(payload);
require(success, "Alert failed");
}
}

之前的报错

错误答案:

1
2
3
4
5
6
7
8
// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.20;
contract Sidekick {
function sendAlert(address hero, uint enemies, bool armed) external {
bytes memory payload = abi.encodeWithSignature("alert(uint256,bool)",5,true);
(bool success1, ) = hero.call(payload);
}
}

从你提供的错误信息来看,testAlert2() 测试没有通过,错误表明传递给 hero 合约的敌人数量和敌人是否武装的值与预期不匹配:

  • 错误 1: it should have let the hero know there are 2 enemies — 你传递的敌人数量是 5,但应该是 2。
  • 错误 2: it should have let the hero know the enemies are not armed — 你传递的敌人是否武装是 true,但应该是 false

这意味着你的 sendAlert 函数只发送了固定的值 5true,没有根据测试要求传递正确的动态值。

解决方法:

你需要调整 Sidekick 合约中的 sendAlert 函数,使其能够根据传入的 enemiesarmed 参数来动态生成 calldata,并将这些值传递给 hero 合约。这样,testAlert2() 测试就会传递正确的敌人数量和武装状态。

修改后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Sidekick {
// sendAlert 函数根据传入的参数生成 calldata
function sendAlert(address hero, uint enemies, bool armed) external {
// 使用传入的 enemies 和 armed 参数构造 calldata
bytes memory payload = abi.encodeWithSignature("alert(uint256,bool)", enemies, armed);

// 调用 hero 合约的 alert 函数
(bool success, ) = hero.call(payload);
require(success, "Alert failed");
}
}

解释:

  • 在原来的代码中,sendAlert 函数总是传递固定的 5true。但我们需要动态传递 enemiesarmed 参数,因此我们使用 abi.encodeWithSignature 来根据传入的参数构造正确的 calldata。
  • 通过 hero.call(payload) 调用 hero 合约的 alert 函数,传递动态的 enemiesarmed 参数。

进一步说明:

  • abi.encodeWithSignature("alert(uint256,bool)", enemies, armed): 这里我们构造了一个 calldata,用于调用 alert(uint256,bool) 函数。我们用传入的 enemiesarmed 参数来替代原来的固定值 5true,确保每次调用时传递正确的值。
  • (bool success, ) = hero.call(payload): 使用 call 机制调用目标合约的 alert 函数,并传递正确的 calldata。

总结:

通过上述修改,Sidekick 合约的 sendAlert 函数将根据输入动态生成正确的 calldata,并传递给 hero 合约。这样,testAlert2() 测试将能够通过,传递正确的敌人数量和敌人是否武装的状态。

Your Goal: Pass Calldata 你的目标:传递调用数据

The Sidekick needs to be able to relay any calldata along to the Hero. Update the relay function to take the data and send it to the Hero as calldata.
Sidekick需要能够将任何调用数据转发给Hero。更新relay函数以接收data并将其作为调用数据发送给Hero。

答案:

1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Sidekick {
function relay(address hero, bytes memory data) external {
// send all of the data as calldata to the hero
(bool success, ) = hero.call(data);
require(success, "Relay failed");
}
}

Your Goal: Trigger the fallback 你的目标:传递调用数据

回退函数 (Fallback Function)

当数据发送到合约时,如果数据与合约中任何函数的“标识符”不匹配(即函数签名的前 4 个字节的哈希值),合约的回退函数(fallback function)将被触发。这意味着,如果你向合约发送一个随机的 4 字节值,它很可能不会匹配任何函数,如果合约中存在回退函数,就会触发回退函数。

同样的,如果你发送的数据字节数少于或多于 4 字节!只要这前 4 字节与某个函数标识符不匹配,回退函数就会被触发。

题目:
In the makeContact method, send some calldata to the Hero contract that will trigger its fallback function.
Sidekick需要能够将任何调用数据转发给Hero。更新relay函数以接收data并将其作为调用数据发送给Hero。

答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Sidekick {
function makeContact(address hero) external {
// TODO: trigger the hero's fallback function!
(bool success, ) = hero.call("data");
require(success, "Relay failed");
}
fallback() external payable {
// 这里可以处理触发回退函数的逻辑
}
}

let’s build an escrow agreement between 2 parties to transfer some value, arbitrated by a 3rd party 解释一下这句话的意思

这句话的意思是:让我们构建一份由第三方仲裁的、双方之间的价值转移托管协议

我们可以拆解一下句子中的关键术语,结合你提到的 Escrow Smart Contract(托管智能合约) 背景来理解:

  1. escrow agreement:托管协议。指交易双方把要转移的资产(比如资金、代币、数字资产等)先交给一个中立的第三方保管,满足约定条件后,第三方再把资产转给接收方。
  2. between 2 parties:在双方之间。这里的双方就是交易的发起方和接收方,比如买家和卖家。
  3. to transfer some value:用以转移一定的价值。这里的“价值”可以是货币、加密货币、数字资产,或是其他有价值的标的物。
  4. arbitrated by a 3rd party:由第三方仲裁。第三方扮演“托管人”或“仲裁者”的角色,它需要确保交易双方都遵守协议条款——比如买家付款后,卖家是否履约发货;如果双方产生纠纷,也由这个第三方来判定资产最终归属。

在区块链和智能合约的场景下,这个“第三方”通常就是托管智能合约,它会按照预先编写好的代码逻辑自动执行托管、验证条件和完成资产转移,无需人工干预,能有效降低信任成本。

我可以帮你整理一份简单的托管智能合约核心逻辑清单,让你更直观地理解这种三方模式的运作步骤,需要吗?

Your Goal: Addresses 你的目标:地址

Create three public storage variables for the addresses of the depositor, beneficiary and arbiter.
为depositor、beneficiary和arbiter的地址创建三个公共存储变量。

答案:

1
2
3
4
5
6
7
8
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

contract Escrow {
address public depositor;
address public beneficiary;
address public arbiter;
}

存款人是部署合约的地址,因此获取该地址并将其存储在depositor状态变量中。部署合约的地址不就是address(this)合约的地址吗?

你的核心疑问是 “部署合约的地址” 和 “合约自身地址” 的混淆 —— 这是 Solidity 中最基础也最容易踩坑的地址概念,必须明确区分:

关键结论先摆清楚

  • 部署合约的地址 = msg.sender(发起部署交易的外部账户/合约)
  • 合约自身的地址 = address(this)(合约部署后在链上的唯一标识)
  • 两者永远不是同一个地址(除非极端特殊场景,且无实际意义)

用通俗的例子解释

把合约想象成「一个快递柜」:

  1. 你(比如地址 0x123)去快递柜公司申请部署一个新快递柜 → 你就是「部署者」(msg.sender
  2. 快递柜安装好后,有了自己的物理编号(比如 0x456)→ 这是「快递柜自身地址」(address(this)
  3. 你(0x123)往快递柜(0x456)里放包裹 → 你是「存款人」,快递柜是「资金托管容器」

显然:你(部署者)和快递柜(合约)是两个完全不同的主体,地址自然不一样。

回到你的代码误区

你原来的代码写 depositor = address(this),相当于:

  • 把「快递柜自身」当成了「放包裹的人」→ 逻辑完全错误
  • 正确逻辑是「放包裹的人(你,msg.sender)」才是 depositor

用代码验证(一看就懂)

部署以下测试合约,就能直观看到两者的区别:

1
2
3
4
5
6
7
8
9
10
11
pragma solidity 0.8.20;

contract TestAddress {
address public deployer; // 部署合约的地址(msg.sender)
address public contractAddress; // 合约自身地址(address(this))

constructor() {
deployer = msg.sender; // 部署者地址
contractAddress = address(this); // 合约自身地址
}
}

部署后调用两个查询函数:

  • deployer 会返回 你的钱包地址(比如 MetaMask 账户地址)
  • contractAddress 会返回 合约部署后的链上地址(一串全新的地址)
  • 两者必然不同,这就是你之前报错的根本原因(工具预期 depositor 是部署者地址,而你存的是合约地址)

补充:address(this) 的正确用法

address(this) 是「合约自身的地址」,核心用途是:

  1. 查询合约的 ETH 余额:address(this).balance(比如托管合约中查询当前托管的资金)
  2. 合约向其他地址转账时,作为「付款方」(比如 (bool success, ) = beneficiary.call{value: address(this).balance}("")
  3. 引用合约自身的其他功能(极少用,一般用 this 直接调用函数)

总结

  • 部署合约的地址 ≠ 合约自身的地址
  • 部署者 = msg.sender(发起部署交易的账户)
  • 合约自身 = address(this)(合约在链上的身份)
  • 你的需求是「存款人 = 部署者」,所以必须用 depositor = msg.sender,而不是 address(this)

Your Goal: Store Addresses 你的目标:存储地址

Create a constructor which takes two arguments: an address for the arbiter and an address for the beneficiary (in that order). Store these variables in the corresponding state variables.
创建一个constructor,它接受两个参数:一个用于仲裁者的address和一个用于受益人的address(按此顺序)。将这些变量存储在相应的状态变量中。
The depositor is the address deploying the contract, so take this address and store it in the depositor state variable.
存款人是部署合约的地址,因此获取该地址并将其存储在depositor状态变量中。

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

contract Escrow {
address public depositor;
address public beneficiary;
address public arbiter;
constructor(address _arbiter, address _beneficiary) {
arbiter = _arbiter;
beneficiary = _beneficiary;
depositor = msg.sender;
}
}

Your Goal: Payable 你的目标:存储地址

Modify the constructor function to make it payable.
修改构造函数,使其可支付。

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

contract Escrow {
address public depositor;
address public beneficiary;
address public arbiter;
constructor(address _arbiter, address _beneficiary) payable {
arbiter = _arbiter;
beneficiary = _beneficiary;
depositor = msg.sender;
}
}

Your Goal: Approve 你的目标:批准

Create an external function called approve.
创建一个名为approve的外部函数。
This function should move the contract’s balance to the beneficiary’s address.
此函数应将合约余额转移到受益人的地址。

答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

contract Escrow {
address public depositor;
address public beneficiary;
address public arbiter;
constructor(address _arbiter, address _beneficiary) payable {
arbiter = _arbiter;
beneficiary = _beneficiary;
depositor = msg.sender;
}
function approve() external {
uint256 amount = address(this).balance;
(bool success, ) = payable(beneficiary).call{value: amount}("");
require(success);
}
}

请问实现approve函数有几种方法?

我记得可以用transfer,我以前学习erc20合约标准的时候有学过,transfer函数和transferFrom的区别,我记得第二种方法是使用payable


在Solidity中实现 approve 函数(核心功能:将合约余额转移给受益人),主要有 3种核心实现方式,其中你提到的 transferpayable 是关键技术点(注:transferFrom 是ERC20的授权转账,不适用于合约余额直接转移,后文会明确区分)。以下是详细说明:

一、先明确核心前提

  1. 合约余额:指合约中存储的 原生代币(ETH/BNB等),而非ERC20代币(ERC20代币的转账需用 transfer/transferFrom,但合约“余额”默认指原生代币)。
  2. 受益人地址:必须是 payable 类型(才能接收原生代币)。
  3. approve 函数的权限:通常需要 管理员/授权账户 调用(避免任何人随意转走合约余额,下文示例会加入权限控制)。

二、3种实现方法(按安全性优先级排序)

方法1:使用 transfer(最安全,推荐)

transfer 是Solidity内置的原生代币转账函数,特点:

  • 固定转账2300 gas(防止重入攻击);
  • 转账失败时直接 回滚交易(无需手动处理返回值);
  • 语法:beneficiary.transfer(amount)beneficiary 必须是 payable 地址)。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract ApproveContract {
address public immutable owner; // 管理员(有权调用approve)
address public immutable beneficiary; // 固定受益人(也可设为参数)

// 构造函数:初始化管理员和受益人
constructor(address _beneficiary) {
owner = msg.sender;
beneficiary = payable(_beneficiary); // 强制转为payable
}

// 权限修饰符:仅管理员可调用
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}

// 外部函数:将合约全部余额转移给受益人
function approve() external onlyOwner {
uint256 contractBalance = address(this).balance; // 获取合约余额
require(contractBalance > 0, "Contract has no balance");

// 核心:用transfer转账(失败自动回滚)
payable(beneficiary).transfer(contractBalance);
// 注:Solidity 0.8.0+ 中,address类型需显式转为payable(即使构造函数已转)
}

// 接收原生代币的函数(合约需有此函数才能接收转账)
receive() external payable {}
}

方法2:使用 call{value: amount}("")(最灵活,推荐)

call 是低级别转账函数,特点:

  • 可自定义gas上限(默认使用剩余gas);
  • 转账失败时返回 bool 值(需手动检查,否则交易不会回滚);
  • 支持携带额外数据(此处用空字符串 "" 表示仅转账);
  • 安全性:需配合 ReentrancyGuard 防止重入攻击(Solidity 0.8.0+ 内置溢出检查,无需额外处理)。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; // 防重入库

// 继承ReentrancyGuard实现防重入
contract ApproveContractWithCall is ReentrancyGuard {
address public immutable owner;
address public immutable beneficiary;

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

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

// 外部函数:转账(加nonReentrant防重入)
function approve() external onlyOwner nonReentrant {
uint256 contractBalance = address(this).balance;
require(contractBalance > 0, "Contract has no balance");

// 核心:用call转账,必须检查返回值
(bool success, ) = payable(beneficiary).call{value: contractBalance}("");
require(success, "Transfer failed"); // 转账失败则回滚
}

receive() external payable {}
}

方法3:使用 send(不推荐,仅作了解)

send 是早期的转账函数,特点:

  • 固定转账2300 gas(同transfer);
  • 转账失败时返回 bool 值(需手动检查,否则交易不会回滚);
  • 安全性:不如 transfer(失败不自动回滚,容易被忽略错误)。

实现代码(仅示例,不推荐生产使用)

1
2
3
4
5
6
7
8
function approve() external onlyOwner {
uint256 contractBalance = address(this).balance;
require(contractBalance > 0, "Contract has no balance");

// 核心:用send转账,必须检查返回值
bool success = payable(beneficiary).send(contractBalance);
require(success, "Transfer failed");
}

三、关键概念澄清(你提到的ERC20相关疑问)

你提到的 transfertransferFromERC20代币的转账函数,与本文“合约原生代币余额转移”完全不同,需明确区分:

函数 适用场景 核心逻辑
transfer(原生代币) 合约→受益人(原生代币) payable(address).transfer(amount),失败自动回滚
transfer(ERC20) 代币持有者→接收者 IERC20(token).transfer(to, amount),需授权(无)
transferFrom(ERC20) 授权方→接收者(代转账) 需先通过 approve 授权,再由第三方调用转账

简单说:

  • 本文的 approve 函数是 转移合约的原生代币,用 transfer/call 即可;
  • ERC20的 approve授权第三方操作自己的代币,后续用 transferFrom 执行转账。

四、总结与推荐

  1. 推荐使用方法1(transfer):简单、安全,适合大多数场景(无需自定义gas);
  2. 推荐使用方法2(call):适合需要自定义gas、携带数据的场景(需配合防重入);
  3. 避免使用方法3(send):错误处理繁琐,安全性不足;
  4. 必须加 权限控制(如 onlyOwner),否则任何人都能转走合约余额;
  5. 合约需实现 receive()fallback() 函数,才能接收原生代币(否则无法存入余额)。