函数
函数是一组可重用代码的包装,可接受参数作为输入,可返回输出。函数也被称为代码的可执行单元。
函数的定义
函数由关键字 function
声明,后面跟函数名、参数、可视范围、状态可变性、返回值的定义。函数可以定义在合约内部,也可以定义在合约外部。
function fnName(<parameter list>)
<visibility>
<state mutability>
[returns(<return type>)] {
//语句
}
function fnName(<parameter types>)
{internal|external}
[pure|view|payable]
[returns (<return types>)]{
//...
}
function: 声明函数的固定关键字
fnName : 函数名,推荐小驼峰写法,更多参考: 合约编码规范 (
TODO:
)<parameter list>
: 参数列表(参数类型 + 参数名字)<visibility>
: 可见性public
external
internal
private
<state mutability>
: 状态可变性pure
view
payable
不写
pure/view/payable
中任何一个,代表,函数既可以读取也可以写入状态变量。
returns (<return types>)
:返回值和返回参数类型
合约内的函数
注意:可以在合约内部或外部定义函数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Fun {
function add(uint256 x, uint256 y) external pure returns (uint256) {
return x + y;
}
function minus(uint256 x, uint256 y) external pure returns (uint256) {
return x - y;
}
}
合约外的函数
合约之外的函数(也称为“自由函数”)始终具有隐式的 internal
可见性。 它们的代码包含在所有调用它们合约中,类似于内部库函数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
function sum(uint256[] memory arr) pure returns (uint256 s) {
for (uint256 i = 0; i < arr.length; i++) {
s += arr[i];
}
}
contract ArrayExample {
bool public found;
function f(uint256[] memory arr) public {
// 在内部调用 free 函数。编译器会将其代码添加到合约中。
uint256 s = sum(arr);
found = s >= 10 ? true :false;
}
}
在合约之外定义的函数仍然在合约的上下文内执行。他们仍然可以访问变量 this
,也可以调用其他合约,将其发送以太币或销毁调用它们合约等其他事情。
与在合约中定义的函数的主要区别为:自由函数不能直接访问存储变量和不在他们的作用域范围内函数。
函数的输入参数
函数参数的声明方式与变量相同。不过未使用的参数可以省略参数名。
例如,如果我们希望合约接受有两个整数形参的函数的外部调用,可以像下面这样写:
普通用法
函数参数可以当作为本地变量
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Simple {
uint256 sum;
function add(uint256 a, uint256 b) public {
sum = a + b;
}
}
函数参数可用在等号左边被赋值。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Simple {
function demo(uint256 _a) public pure returns (uint256) {
_a = 22;
return _a;
}
}
可以使用数组作为函数参数:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Simple {
uint256[] a;
function demo(uint256[] memory _a) public returns (uint256[] memory) {
a = _a;
return _a;
}
}
不同版本的对比:二维数组
注解:0.8.0 之前外部函数不可以接受多维数组作为参数 如果原文件加入 pragma abicoder v2;
可以启用 ABI v2 版编码功能,这此功能可用。(注:在 0.7.0 之前是使用 pragma experimental ABIEncoderV2;
)
内部函数 则不需要启用 ABI v2 就接受多维数组作为参数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
pragma experimental ABIEncoderV2;
contract Simple {
uint256[][2] a = [[1, 2], [3, 4]];
function demo() public view returns (uint256[][2] memory) {
return a;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Simple {
uint256[][2] a = [[1, 2], [3, 4]];
function demo() public view returns (uint256[][2] memory) {
return a;
}
}
不同版本的对比:自定义结构
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
pragma experimental ABIEncoderV2;
contract Simple {
struct BillType {
uint256 duration;
uint256 multiplier;
}
BillType[] public allBillTypes;
// constructor() public {
// allBillTypes.push(BillType({duration: 1, multiplier: 1}));
// }
function getAllBillType() public view returns (BillType[] memory) {
return allBillTypes;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Simple {
struct BillType {
uint256 duration;
uint256 multiplier;
}
BillType[] public allBillTypes;
constructor() {
allBillTypes.push(BillType({duration: 1, multiplier: 1}));
}
function getAllBillType() public view returns (BillType[] memory) {
return allBillTypes;
}
}
在 0.8.X 版本已经没有问题了。
函数的调用
要调用函数,只需使用函数名,并传入参数即可。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
mapping(address => uint256) public balances;
event WithdrawAll(uint256 amount);
// 存款
function deposit() public payable {
balances[msg.sender] += msg.value;
}
// 取款
function withdrawAll() public {
uint256 amountBalance = amountAvailable();
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amountBalance}("");
require(success, "Failed to send");
emit WithdrawAll(amountBalance);
}
// 扣除 1000 wei 作为合约的使用手续费
function amountAvailable() public view returns (uint256) {
require(balances[msg.sender] > 1000, "must > 1000");
return balances[msg.sender] - 1000;
}
}
上面合约种的 amountAvailable
既可以在合约外,用户直接调用,也可以在合约内部 withdrawAll
中使用。
构造函数
构造函数关键字 constructor
,Solidity 构造函数是一个特殊函数,它仅能在智能合约部署的时候调用一次,创建之后就不能再次被调用。
构造函数是可选的,只允许有一个构造函数,这意味着不支持重载。
用处: Solidity 构造函数常用来进行状态变量的初始化工作。
比如设置合约的 owner 权限
设置状态变量的初始值
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract ErrorModifier{
address public owner;
uint public count = 0;
constructor(uint _x){
owner = msg.sender;
count = _x;
}
}
需要注意的是:在合约创建的过程中,它的代码还是空的,所以直到构造函数执行结束,我们都不应该在其中调用合约自己的函数。(我们可以调用,但是不推荐调用)
请注意:不可以在构造函数中通过 this 来调用函数,因为此时真实的合约实例还没有被创建。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract ErrorModifier {
address public owner;
uint256 public count = 0;
constructor(uint256 _x) {
owner = msg.sender;
count = _x;
test();
}
function test() public returns (uint256) {
count = 1;
return count;
}
}
visibility:可见性
可见性标识符的定义位置,对于状态变量来说是在类型后面,对于函数是在参数列表和返回关键字中间。
函数对不同的合约有不同的可见性。visibility 又称为:可视范围/可见性/作用域,函数的可视范围有四种:
Private(私有):函数只能在所定义的智能合约内部调用。
在继承的合约内不可访问。
Internal(内部):可以在所定义智能合约内部调用该函数,也可以从继承合约中调用该函数。
internal 函数和状态变量可以在当前合约或继承合约里调用。需要注意的是不能加前缀 this,前缀 this 是表示通过外部方式访问。
External(外部):只能从智能合约外部调用。 如果要从智能合约中调用它,则必须使用
this
。外部函数是合约接口的一部分,所以我们可以从其它合约或通过交易来发起调用。一个外部函数 f,不能通过内部的方式来发起调用,如 f()不可以调用,但可以通过 this.f()。
外部函数在接收大的数组数据时更加有效。
Public(公开):可以从任何地方调用。
内部函数只能在当前合约内被调用(更具体来说,在当前代码块内,包括内部库函数和继承的函数中),因为它们不能在当前合约上下文的外部被执行。
调用一个内部函数是通过跳转到它的入口标签来实现的,就像在当前合约的内部调用一个函数。
外部函数由一个地址和一个函数签名组成,可以通过外部函数调用传递或者返回。
函数类型默认是内部函数,因此不需要声明 internal
关键字。请注意,这仅适用于函数类型,合约中定义的函数明确指定可见性,它们没有默认值。
更多内容参考 变量的作用域&可视范围
private
private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract C {
function f(uint a) private pure returns (uint b) { return a + 1; }
function setData(uint a) internal { data = a; }
uint public data;
}
在下面的例子中,D
可以调用 c.getData()
来获取状态存储中 data
的值,但不能调用 f
。 合约 E
继承自 C
,因此可以调用 compute
。
pragma solidity >=0.4.16 <0.9.0;
contract C {
uint private data;
function f(uint a) private returns(uint b) { return a + 1; }
function setData(uint a) public { data = a; }
function getData() public returns(uint) { return data; }
function compute(uint a, uint b) internal returns (uint) { return a+b; }
}
// 下面代码编译错误
contract D {
function readData() public {
C c = new C();
uint local = c.f(7); // 错误:成员 `f` 不可见
c.setData(3);
local = c.getData();
local = c.compute(3, 5); // 错误:成员 `compute` 不可见
}
}
contract E is C {
function g() public {
C c = new C();
uint val = compute(3, 5); // 访问内部成员(从继承合约访问父合约成员)
}
}
警告: 设置为 private
或 internal
,只能防止其他合约读取或修改信息,但它仍然可以在链外查看到。
internal
一个内部函数可以被分配给一个内部函数类型的变量,无论定义在哪里,包括合约和库的私有、内部和 public 函数,以及自由函数。
另一方面,外部函数类型只与 public 和外部合约函数兼容。库是不可以的,因为库使用 delegatecall,并且 他们的函数选择器有不同的 ABI 转换 。 接口中声明的函数没有定义,所以指向它们也没有意义。
内部可见性函数访问可以在当前合约或派生的合约访问,不可以外部访问。 由于它们没有通过合约的 ABI 向外部公开,它们可以接受内部可见性类型的参数:比如映射或存储引用。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
library ArrayUtils {
// 内部函数可以在内部库函数中使用,
// 因为它们会成为同一代码上下文的一部分
function map(
uint256[] memory self,
function(uint256) pure returns (uint256) f
) internal pure returns (uint256[] memory r) {
r = new uint256[](self.length);
for (uint256 i = 0; i < self.length; i++) {
r[i] = f(self[i]);
}
}
function reduce(
uint256[] memory self,
function(uint256, uint256) pure returns (uint256) f
) internal pure returns (uint256 r) {
r = self[0];
for (uint256 i = 1; i < self.length; i++) {
r = f(r, self[i]);
}
}
function range(uint256 length) internal pure returns (uint256[] memory r) {
r = new uint256[](length);
for (uint256 i = 0; i < r.length; i++) {
r[i] = i;
}
}
}
contract Pyramid {
using ArrayUtils for *;
function pyramid(uint256 l) public pure returns (uint256) {
return ArrayUtils.range(l).map(square).reduce(sum);
}
function square(uint256 x) internal pure returns (uint256) {
return x * x;
}
function sum(uint256 x, uint256 y) internal pure returns (uint256) {
return x + y;
}
}
当前合约中的函数可以直接(“从内部”)调用,也可以递归调用,就像下边这个无意义的例子一样。(输出"uint256: ret 0"
)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
// 编译器会有警告提示
contract C {
function g(uint256 a) public pure returns (uint256 ret) {
return f();
}
function f() internal pure returns (uint256 ret) {
return g(7) + f();
}
}
这些函数调用在 EVM 中被解释为简单的跳转。这样做的效果就是当前内存不会被清除,例如,函数之间通过传递内存引用进行内部调用是非常高效的。 只能在同一合约实例的函数,可以进行内部调用。
只有在同一合约的函数可以内部调用。仍然应该避免过多的递归调用, 因为每个内部函数调用至少使用一个堆栈槽, 并且最多有 1024 堆栈槽可用。
external
external 外部可见性函数作为合约接口的一部分,意味着我们可以从其他合约和交易中调用。 一个外部函数 f
不能从内部调用(即 f
不起作用,但 this.f()
可以)。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Oracle {
struct Request {
bytes data;
function(uint256) external callback;
}
Request[] private requests;
event NewRequest(uint256);
function query(bytes memory data, function(uint256) external callback)
public
{
requests.push(Request(data, callback));
emit NewRequest(requests.length - 1);
}
function reply(uint256 requestID, uint256 response) public {
// 这里检查回复来自可信来源
requests[requestID].callback(response);
}
}
contract OracleUser {
Oracle private constant ORACLE_CONST =
Oracle(address(0x00000000219ab540356cBB839Cbe05303d7705Fa)); // known contract
uint256 private exchangeRate;
function buySomething() public {
ORACLE_CONST.query("USD", this.oracleResponse);
}
function oracleResponse(uint256 response) public {
require(
msg.sender == address(ORACLE_CONST),
"Only oracle can call this."
);
exchangeRate = response;
}
}
方式也可以使用表达式 this.g(8)
; 和 c.g(2)
; 进行调用,其中 c 是合约实例, g 合约内实现的函数,但是这两种方式调用函数,称为“外部调用”,它是通过消息调用来进行,而不是直接的代码跳转。请注意,不可以在构造函数中通过 this
来调用函数,因为此时真实的合约实例还没有被创建。
如果想要调用其他合约的函数,需要外部调用。对于一个外部调用,所有的函数参数都需要被复制到内存。
从一个合约到另一个合约的函数调用不会创建自己的交易, 它是作为整个交易的一部分的消息调用。
调用函数并转账
当调用其他合约的函数时,需要在函数调用是指定发送的 Wei 和 gas 数量,可以使用特定选项 {value: 10, gas: 10000}
,请注意,不建议明确指定 gas,因为操作码的 gas 消耗将来可能会发生变化。 任何发送给合约 Wei 将被添加到目标合约的总余额中:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract InfoFeed {
function info() external payable returns (uint256 ret) {
return 42;
}
}
contract Consumer {
InfoFeed feed;
function setFeed(InfoFeed addr) public {
feed = addr;
}
function callFeed() public payable{
feed.info{value: 10, gas: 800}();
}
}
payable 修饰符要用于修饰 info 函数,否则, value 选项将不可用。
注意 feed.info{value: 10, gas: 800}
仅(局部地)设置了与函数调用一起发送的 Wei 值和 gas 的数量,只有最后的小括号才执行了真正的调用。 因此, feed.info{value: 10, gas: 800}
是没有调用函数的, value
和 gas
设置是无效的。
extcodesize 操作码来检查要调用的合约是否确实存在
由于 EVM 认为可以调用不存在的合约的调用,因此在 Solidity 语言层面里会使用 extcodesize 操作码来检查要调用的合约是否确实存在(包含代码),如果不存在该合约,则抛出异常。如果返回数据在调用后被解码,则跳过这个检查,因此 ABI 解码器将捕捉到不存在的合约的情况。
请注意,这个检查在 低级 call 时不被执行,这些调用是对地址而不是合约实例进行操作。
当使用高级别的方式调用 预编译合约时 需要注意,因为因为根据上面的逻辑,编译器认为它们不存在,即使它们执行代码并返回数据。
如果被调用合约本身抛出异常或者 gas 用完等,函数调用也会抛出异常。
与其他合约交互时候有什么需要注意的?
任何与其他合约的交互都会产生潜在危险,尤其是在不能预先知道合约代码的情况下。 交互时当前合约会将控制权移交给被调用合约,而被调用合约可能做任何事。即使被调用合约从一个已知父合约继承,继承的合约也只需要有一个正确的接口就可以了。 被调用合约的实现可以完全任意的实现,因此会带来危险。 此外,请小心这个交互调用在返回之前再回调我们的合约,这意味着被调用合约可以通过它自己的函数改变调用合约的状态变量。 一个建议的函数写法是,例如,在合约中状态变量进行各种变化后再调用外部函数,这样,你的合约就不会轻易被滥用的重入攻击 (reentrancy) 所影响
public
请注意,当前合约的 public 函数既可以被当作内部函数也可以被当作外部函数使用。 如果想将一个函数当作内部函数使用,就用 f
调用,如果想将其当作外部函数,使用 this.f
。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Example {
function f() public payable returns (bytes4) {
assert(this.f.address == address(this));
return this.f.selector;
}
function g() public {
this.f{gas: 10, value: 800}();
}
}
public & external 函数的属性
public(或 external)函数都有下面的成员:
.address
返回函数的合约地址。.selector
返回 ABI 函数选择器
由于 Solidity 有两种函数调用:外部调用则会产生一个 EVM 调用,而内部调用不会。
例子: internal
和 external
搭配做 DAO 的管理员的小案例。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract DAO {
// 状态变量
mapping(address => bool) public admins;
// 事件
event AddAdmin(address indexed ads);
// 函数修改器
modifier onlyRole() {
require(admins[msg.sender], "Not Authorized");
_;
}
constructor() {
_addAdmin(msg.sender);
}
function _addAdmin(address _ads) internal {
admins[_ads] = true;
emit AddAdmin(_ads);
}
function addAdmin(address _ads) external onlyRole {
_addAdmin(_ads);
}
}
例子小结:
该特性常用于权限的区分;
比如
internal _fn
用于在constructor
内使用。而external fn
用于外部带验证使用,并且在fn
内部调用_fn
;
例子:合约继承的例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract A {
uint256 private varPrivate = 0;
uint256 internal varInternal = 1;
// 不能声明 external 可见性的状态变量
// uint256 external varExternal = 2; // Expected identifier but got 'external'
uint256 public varPublic = 3;
function testPrivate() private pure returns (uint256) {
return 0;
}
function testInternal() internal pure returns (uint256) {
return 1;
}
function testExternal() external pure returns (uint256) {
return 2;
}
function testPublic() public pure returns (uint256) {
return 3;
}
}
contract B is A {
function getVal() external view returns (uint256, uint256) {
// return varPrivate; // private 仅在A内,不能被继承,无法访问
return (varInternal, varPublic);
}
function getFn()
external
view
returns (
uint256,
uint256,
uint256
)
{
// uint256 p = testPrivate(); // private 仅在A内,不能被继承,无法访问
uint256 i = testInternal(); // 可访问 internal 函数
// external 只能在外部访问,不能直接访问 external 函数
// uint256 e = testExternal();
// 虽然不能直接访问,但是可以使用 this 代表合约外部,
// 然后调用外部函数;该方法不推荐。
uint256 e2 = this.testExternal();
uint256 p2 = this.testPublic();
return (i, e2, p2);
}
}
总结:
/**
-----------------------
| contract A |
| |
| 有效变量类型: |
| varPrivate |
| varInternal |
| varPublic | <------------ 外部
| 不存在external变量 | 变量: varPublic
| | 方法: testPublic / testExternal
| 有效函数类型: |
| testPrivate |
| testInternal |
| testExternal |
| testPublic |
-----------------------
-----------------------
| contract B |
| |
| 有效变量类型: |
| varInternal |
| varPublic | <------------ 外部
| | 变量: varPublic
| | 方法: testPublic / testExternal
| 有效函数类型: |
| testInternal |
| testPublic |
-----------------------
*/
mutability:状态可变性
pure
: 既不读取也不修改状态变量这种函数被称为纯函数
view
: 读取状态变量,但是不修改状态变量这种函数被称为视图函数
状态变量的 Getter 方法默认是 view 函数。
payable
:用 payable 声明的函数可以接受发送给合约的以太币.如果未指定,该函数将自动拒绝所有发送给它的以太币
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract ViewAndPure{
uint public num ;
function viewFn() external view returns(uint){
return num;
}
function pureFn(uint x) external pure returns(uint){
return x + 1;
}
}
pure 不允许的操作
声明为 pure 函数,可以在函数声明里,添加 pure 关键字。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract C {
function f(uint256 a, uint256 b) public pure returns (uint256) {
return a * (b + 42);
}
}
如果函数中存在以下语句,则被视为读取状态,编译器将抛出警告。
读取状态变量。
这也意味着读取
immutable
变量也不是一个pure
操作。
访问
address(this).balance
或<address>.balance
访问
block
,tx
,msg
中任意成员 (除msg.sig
和msg.data
之外)。调用任何未标记为
pure
的函数。使用包含特定操作码的内联汇编。
TODO:
这个不了解,需要用例子加深印象。
使用操作码
STATICCALL
, 这并不保证状态未被读取, 但至少不被修改。
如果发生错误,pure
函数可以使用 revert()
和 require()
函数来还原潜在的状态更改。还原状态更改不被视为 状态修改, 因为它只还原以前在没有view
或 pure
限制的代码中所做的状态更改, 并且代码可以选择捕获 revert 并不传递还原。这种行为也符合 STATICCALL 操作码。
警告:不可能在 EVM 级别阻止函数读取状态, 只能阻止它们写入状态 (即只能在 EVM 级别强制执行 view
, 而 pure
不能强制)。
在 0.5.0 版本之前, 编译器没有对 pure 函数使用 STATICCALL 操作码。这样通过使用无效的显式类型转换启用 pure 函数中的状态修改。 通过对 pure 函数使用 STATICCALL , 可以防止在 EVM 级别上对状态进行修改。
在 0.4.17 版本之前,编译器不会强制 pure 函数不读取状态。它是一个编译时类型检查, 可以避免在合约类型之间进行无效的显式转换, 因为编译器可以验证合约类型没有状态更改操作, 但它不会在运行时能检查调用实际的类型。
view 不允许的操作
可以将函数声明为 view 类型,这种情况下要保证不修改状态。声明为 view 图函数,可以在函数声明里,添加 view 关键字。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract C {
function f(uint256 a, uint256 b) public view returns (uint256) {
return a * (b + 42) + block.timestamp;
}
}
注解: Getter 方法自动被标记为 view。
如果函数中存在以下语句,则被视为修改状态,编译器将抛出警告。
修改状态变量。
触发事件。
创建其它合约。
使用
selfdestruct
。通过调用发送以太币。
调用任何没有标记为 view 或者 pure 的函数。
使用底层调用
(TODO:这里是 call 操作么?)
使用包含某些操作码的内联程序集。
payable
一个加和减的 DEMO
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Counter{
uint public count ;
function add() external {
count+=1;
}
function minus() external {
count-=1;
}
}
状态可变性的类型转换
如果满足下列条件,函数类型 A 可以隐式转换为函数类型:
它们的参数类型相同,返回类型相同,它们的内部/外部属性是相同的,并且 A 的状态可变性比 B 的状态可变性更具限制性
比如:
pure 函数可以转换为 view 和 non-payable 函数
view 函数可以转换为 non-payable 函数
payable 函数可以转换为 non-payable 函数
其他的转换则不可以。
关于 payable
和 non-payable
的规则可能有点令人困惑,但实质上,如果一个函数是 payable
,这意味着它 也接受零以太的支付,因此它也是 non-payable
。 另一方面,non-payable
函数将拒绝发送给它的 以太币 Ether , 所以 non-payable
函数不能转换为 payable
函数。
函数的返回值 returns/return
函数返回类型不能为空 —— 如果函数类型不需要返回,则需要删除整个 returns (<return types>)
部分。
函数可能返回任意数量的参数作为输出。函数的返回值有两个关键字,一个是returns
,一个是 return
;
returns
是在函数名后面的,用来标示返回值的数量,类型,名字信息。return
是在函数主体内,用于返回returns
指定的数据信息
多种返回值
函数返回变量的声明方式在关键词 returns
之后,与参数的声明方式相同。函数有以下几个返回值。
单个返回值
多个返回值
带有名字的返回值
隐式返回
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract FunctionOutputs {
// 单个返回值
function returnSingle() public pure returns (uint256) {
return 1;
}
// 多个返回值
function returnMultiple() public pure returns (uint256, bool) {
return (1, true);
}
// 带有名字的返回值
// 这个形式等同于赋值给返回参数,然后用 return; 退出。
function returnName() public pure returns (uint256 u, bool b) {
return (1, true);
}
// 隐式返回
function returnAssigned() public pure returns (uint256 u, bool b) {
u = 1;
b = true;
}
}
返回变量名可以被省略。 返回变量可以当作为函数中的本地变量,没有显式设置的话,会使用 :默认值。返回变量可以显式给它附一个值
如果使用 return 提前退出有返回值的函数, 必须在用 return 时提供返回值。
注解:非内部函数有些类型没法返回,比如限制的类型有:多维动态数组、结构体等。如果添加 pragma abicoder v2;
启用 ABI V2 编码器,则是可以的返回更多类型,不过 mapping 仍然是受限的。
小测试:如下合约,test 调用后返回什么
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
//当给返回值赋值后,并且有个return,以最后的return为主
function test() public pure returns (uint256 mul) {
uint256 a = 10;
mul = 100;
return a;
}
}
答案: 返回结果是: 0:uint256: mul 10
合约内函数返回值的接收
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract FunctionOutputs {
// 多个返回值
function returnMultiple() public pure returns (uint256, bool) {
return (1, true);
}
function a() external pure returns (uint256 u, bool b) {
// 接收返回值
(uint256 uu, bool bb) = returnMultiple();
// 只接受一个返回值
(, bool bbb) = returnMultiple();
// 接收并返回
(u, b) = returnMultiple();
}
}
解构赋值和返回多值
Solidity 内部允许元组 (tuple) 类型,也就是一个在编译时元素数量固定的对象列表,列表中的元素可以是不同类型的对象。这些元组可以用来同时返回多个数值,也可以用它们来同时给多个新声明的变量或者既存的变量(或通常的 LValues):
pragma solidity >=0.5.0 <0.9.0;
contract C {
uint index;
function f() public pure returns (uint, bool, uint) {
return (7, true, 2);
}
function g() public {
//基于返回的元组来声明变量并赋值
(uint x, bool b, uint y) = f();
//交换两个值的通用窍门——但不适用于非值类型的存储 (storage) 变量。
(x, y) = (y, x);
//元组的末尾元素可以省略(这也适用于变量声明)。
(index,,) = f(); // 设置 index 为 7
}
}
不可能混合变量声明和非声明变量复制, 即以下是无效的: (x, uint y) = (1, 2);
在 0.5.0 版本之前,给具有更少的元素数的元组赋值都可以可能的,无论是在左边还是右边(比如在最后空出若干元素)。现在,这已经不允许了,赋值操作的两边应该具有相同个数的组成元素。
当涉及引用类型时,在同时分配给多个变量时要小心, 因为这可能会导致意外的复制行为。
数组和结构体的复杂性
TODO:
赋值语义对于像数组和结构体(包括 bytes 和 string) 这样的非值类型来说会有些复杂。参考 数据位置及赋值行为 了解更多 。
在下面的示例中, 对 g(x)
的调用对 x
没有影响, 因为它在内存中创建了存储值独立副本。但是, h(x)
成功修改 x
, 因为只传递引用而不传递副本。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract C {
uint[20] public x;
function f() public {
g(x);
h(x);
}
function g(uint[20] memory y) internal pure {
y[2] = 3;
}
function h(uint[20] storage y) internal {
y[3] = 4;
}
}
函数的签名/函数标识符
在变量的全局变量那一章,我们介绍了 msg.data
msg.sig
,分别是调用合约的完整的 calldata,以及函数标识符(calldata 的前四个字节)
查看 msg.data
代码如下
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Receiver {
event Log(bytes data1, bytes4 data2);
function transfer(address recipient, uint256 amount)
external
payable
returns (address, uint256)
{
emit Log(msg.data, msg.sig);
return (msg.sender, msg.value);
}
}
输入:
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
1
logs 结果:
data1 (为了方便阅读,我拆分成如下)
0xa9059cbb 0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4 0000000000000000000000000000000000000000000000000000000000000001
data2 结果如下
0xa9059cbb
output 结果:
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
2
msg.data 中函数标识符的实现逻辑
核心: bytes4(keccak256(bytes("transfer(address,uint256)")))
一个函数调用数据的前 4 字节,指定了要调用的函数。这就是某个函数签名的 Keccak 哈希的前 4 字节(bytes32 类型是从左取值)。
函数签名被定义为基础原型的规范表达,而基础原型是函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格。.
代码如下,获取 Hash 后的值,和截取后的值
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract FunctionSelector {
function getSelector(string calldata _func)
external
pure
returns (bytes32, bytes4)
{
// _func 字符串通过 bytes 转为 bytes
// 使用 keccak256 进行 Hash值运算
// 使用 bytes4 截取 keccak256 返回的32位数据
return (keccak256(bytes(_func)), bytes4(keccak256(bytes(_func))));
}
}
测试:
部署
输入
"transfer(address,uint256)"
获取结构
0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
0xa9059cbb
注意:以上仅仅是背后的原理展示,如果想要获取值,可以通过.selector
返回 ABI 函数选择器
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
library L {
function f(uint256) external {}
}
contract C {
function g() public pure returns (bytes4) {
return L.f.selector;
}
}
尽管可以对 public 或 external 的库函数进行外部调用,但此类调用会被视为 Solidity 的内部调用,与常规的 contract ABI 规则不同。外部库函数比外部合约函数支持更多的参数类型,例如递归结构和指向存储的指针。
因此,计算用于计算 4 字节选择器的函数签名遵循内部命名模式以及可对合约 ABI 中不支持的类型的参数使用内部编码。
以下标识符可以作为函数签名中的类型:
值类型, 非存储的(non-storage)
string
及非存储的bytes
使用和合约 ABI 中同样的标识符。非存储的数组类型遵循合约 ABI 中同样的规则,例如
<type>[]
为动态数组以及<type>[M]
为M
个元素的动态数组。非存储的结构体使用完整的命名引用,例如
C.S
用于contract C { struct S { ... } }
.存储的映射指针使用
mapping(<keyType> => <valueType>) storage
当<keyType>
和<valueType>
是映射的键和值类型。其他的存储的指针类型使用其对应的非存储类型的类型标识符,但在其后面附加一个空格及
storage
。
函数的重载
Solidity 的函数重载,是指同一个作用域内,相同函数名可以定义多个函数。
这些相同函数名的函数,参数(参数类型或参数数量)必须不一样。,因为只有这样上一节介绍的函数签名中,才能签出来不同的函数选择器。
合约可以具有多个不同参数的同名函数,称为”重载”(overloading),这也适用于继承函数。
下面是两种 sum 方法。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
function sum(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
function sum(uint256[] memory _arr) public pure returns (uint256 temp) {
for (uint256 index = 0; index < _arr.length; index++) {
temp += _arr[index];
}
}
function callSum1() public pure returns (uint256) {
return sum(1, 2);
}
function callSum2() public pure returns (uint256) {
// 下面 nums1 这种动态创建数组的方法是不对的,会报错
// uint256[] memory nums1 = [1, 2, 3];
uint256[] memory nums = new uint256[](5);
nums[0] = 1;
nums[1] = 2;
nums[2] = 3;
nums[3] = 4;
nums[4] = 5;
return sum(nums);
}
}
重载函数也存在于外部接口中。如果两个外部可见函数仅区别于 Solidity 内的类型,而不是它们的外部类型则会导致错误。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// Function overload clash during conversion to external types for arguments.Ï
contract A {
function f(B value) public pure returns (B out) {
out = value;
}
function f(address value) public pure returns (address out) {
out = value;
}
}
contract B {}
以上两个 f
函数重载都接受了 ABI 的地址类型,虽然它们在 Solidity
中被认为是不同的。
选择重载函数 & 参数匹配
选择重载函数:通过将当前范围内的函数声明与函数调用中提供的参数相匹配,这样就可以选择重载函数。
如果所有参数都可以隐式地转换为预期类型,则该函数作为重载候选项。如果一个匹配的都没有,解析失败。
⚠️:返回参数不作为重载解析的依据。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract A {
function f(uint8 val) public pure returns (uint8 out) {
out = val;
}
function f(uint256 val) public pure returns (uint256 out) {
out = val;
}
}
contract B {
A a;
// Member "f" not unique after argument-dependent lookup in contract A.
// function test1() public view returns (uint256) {
// uint256 tar = a.f(8);
// return tar;
// }
function test2() public view returns (uint256) {
uint256 tar = a.f(256);
return tar;
}
}
在 Remix 里,部署 A 合约,会将两个方法都渲染出来,调用 f(50)
/f(256)
都可以。
但是实际调用里,在其他合约内调用 f(50)
会导致类型错误,因为 50
既可以被隐式转换为 uint8
也可以被隐式转换为 uint256
。 另一方面,调用 f(256)
则会解析为f(uint256)
重载,因为 256
不能隐式转换为 uint8
。
modifier:函数修改器
Solidity 中关键字 modifier
用于声明一个函数修改器。
意义:我们可以将一些通用的操作提取出来,包装为函数修改器,来提高代码的复用性,改善编码效率。是函数高内聚,低耦合的延伸。
作用:
modifier
常用于在函数执行前检查某种前置条件。比如地址对不对,余额是否充足,参数值是否允许等
修改器内可以写逻辑
特点:
modifier
是一种合约属性,可被继承,同时还可被派生的合约重写(override)。(修改器 modifier 是合约的可继承属性,并可能被派生合约覆盖 , 但前提是它们被标记为 virtual)。_
符号可以在修改器中出现多次,每处都会替换为函数体。
正常的判断
下面是正常的前置判断,非常的啰嗦。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract ErrorModifier{
address public owner;
uint public count = 0;
constructor(){
owner = msg.sender;
}
function add() external{
require(msg.sender==owner,"must owner address");
count++;
}
function minus() external{
require(msg.sender==owner,"must owner address");
count--;
}
}
函数修改器:普通
将通用的判断抽出为函数修改器。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract ErrorModifier{
address public owner;
uint public count = 0;
constructor(){
owner = msg.sender;
}
// 下面就是函数修改器
modifier onlyOwner(){
require(msg.sender==owner,"must owner address");
_;
}
function add() external onlyOwner{
count++;
}
function minus() external onlyOwner{
count--;
}
}
函数修改器:带参数
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract ErrorModifier{
address public owner;
uint public count = 0;
constructor(){
owner = msg.sender;
}
// 下面就是函数修改器
modifier onlyOwner(){
require(msg.sender==owner,"must owner address");
_;
}
modifier greaterThan(uint _x){
require(_x > 10,"must be greater than 10");
_;
}
function fnA(uint _x) external onlyOwner greaterThan( _x){
count=_x;
}
}
函数修改器:修改器内写逻辑
下面是一个防重载的函数修改器,这种使用方法,在低版本的 solidity 中可以防止重入攻击。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Demo {
bool internal locked;
modifier noReentrant() {
require(!locked, "no reentrant");
locked = true;
_;
locked = false;
}
function test() public noReentrant returns (bool) {
return locked;
}
}
扩展:TODO:
一个 ownable 的 DEMO
这个例子使用了 构造函数 和 函数修改器。这是函数修改器的经典应用,是 OpenZeppelin 库中的 Ownable 合约核心逻辑:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Ownable {
address public owner;
// 发布事件 - 此合约owner已经换人(此逻辑与modifier无关,可以忽略)
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "must owner");
_;
}
modifier notZeroAddress(address _newOwner) {
require(_newOwner != address(0), "invalid address");
_;
}
function transferOwnership(address _newOwner)
external
onlyOwner
notZeroAddress(_newOwner)
{
emit OwnershipTransferred(msg.sender, _newOwner);
owner = _newOwner;
}
function getByOwner() external view onlyOwner returns (address) {
return owner;
}
function getByany() external view returns (address) {
return owner;
}
}
扩展:详细内容,请查看 OpenZeppelin 库中的 Ownable 源码
函数 修改器 的复杂例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract owned {
address owner;
constructor() { owner = payable(msg.sender); }
// 这个合约只定义一个修改器,但并未使用: 它将会在派生合约中用到。
// 修改器所修饰的函数体会被插入到特殊符号 _; 的位置。
// 这意味着如果是 owner 调用这个函数,则函数会被执行,否则会抛出异常。
modifier onlyOwner {
require(
msg.sender == owner,
"Only owner can call this function."
);
_;
}
}
contract destructible is owned {
// 这个合约从 `owned` 继承了 `onlyOwner` 修饰符,并将其应用于 `destroy` 函数,
// 只有在合约里保存的 owner 调用 `destroy` 函数,才会生效。
function destroy() public onlyOwner {
selfdestruct(owner);
}
}
contract priced {
// 修改器可以接收参数:
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
}
contract Register is priced, destructible {
mapping (address => bool) registeredAddresses;
uint price;
constructor(uint initialPrice) { price = initialPrice; }
// 在这里也使用关键字 `payable` 非常重要,否则函数会自动拒绝所有发送给它的以太币。
function register() public payable costs(price) {
registeredAddresses[msg.sender] = true;
}
function changePrice(uint price_) public onlyOwner {
price = price_;
}
}
contract Mutex {
bool locked;
modifier noReentrancy() {
require(
!locked,
"Reentrant call."
);
locked = true;
_;
locked = false;
}
// 这个函数受互斥量保护,这意味着 `msg.sender.call` 中的重入调用不能再次调用 `f`。
// `return 7` 语句指定返回值为 7,但修改器中的语句 `locked = false` 仍会执行。
function f() public noReentrancy returns (uint) {
(bool success,) = msg.sender.call("");
require(success);
return 7;
}
}
如果你想访问定义在合约 C 的 修改器 modifier m , 可以使用 C.m 去引用它,而不需要使用虚拟表查找。
只能使用在当前合约或在基类合约中定义的 修改器 modifier , 修改器 modifier 也可以定义在库里面,但是他们被限定在库函数使用。
如果同一个函数有多个 修改器 modifier,它们之间以空格隔开,修改器 modifier 会依次检查执行。
修改器不能隐式地访问或改变它们所修饰的函数的参数和返回值。 这些值只能在调用时明确地以参数传递。
修改器 modifier 或函数体中显式的 return 语句仅仅跳出当前的 修改器 modifier 和函数体。 返回变量会被赋值,但整个执行逻辑会从前一个 修改器 modifier 中的定义的 _
之后继续执行。
警告:在早期的 Solidity 版本中,有 修改器 modifier 的函数, return 语句的行为表现不同。用 return
; 从修改器中显式返回并不影响函数返回值。 然而,修改器可以选择完全不执行函数体,在这种情况下,返回的变量被设置为默认值,就像该函数是空函数体一样。
_
符号可以在修改器中出现多次,每处都会替换为函数体。
修改器 modifier 的参数可以是任意表达式,在此上下文中,所有在函数中可见的符号,在 修改器 modifier 中均可见。 在 修改器 modifier 中引入的符号在函数中不可见(可能被重载改变)。
全局:数学和密码学函数
在全局命名空间中已经预设了一些特殊的变量和函数,他们主要用来提供关于区块链的信息或一些通用的工具函数。后续会详细介绍,这里简单的介绍几个全局函数
数学和密码学函数
Solidity 也提供了内置的数学和密码学函数:
数学函数:
addmod(uint x, uint y, uint k) returns (uint)
计算
(x + y) % k
,加法会在任意精度下执行,并且加法的结果即使超过2**256
也不会被截取。从 0.5.0 版本的编译器开始会加入对k != 0
的校验(assert)。
mulmod(uint x, uint y, uint k) returns (uint)
计算
(x * y) % k
,乘法会在任意精度下执行,并且乘法的结果即使超过2**256
也不会被截取。从 0.5.0 版本的编译器开始会加入对k != 0
的校验(assert)。
密码学函数:
keccak256((bytes memory) returns (bytes32)
计算 Keccak-256 哈希,之前 keccak256 的别名函数 sha3 在 0.5.0 中已经移除。。
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)
利用椭圆曲线签名恢复与公钥相关的地址,错误返回零值。
函数参数对应于 ECDSA 签名的值:
r = 签名的前 32 字节
s = 签名的第 2 个 32 字节
v = 签名的最后一个字节
ecrecover 返回一个 address, 而不是 address payable。
ecrecover
的使用案例
数学函数
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
function callAddMod() public pure returns (uint256) {
return addmod(4, 5, 3); // 可以直接使用 addmod
}
function callMulMod() public pure returns (uint256) {
return mulmod(4, 5, 3); // 可以直接使用 mulmod
}
}
密码学函数
HASH 的特性
如果输入内容相同,则输出内容必定相同
输入内容的任何变动,都会导致输出结果完全大变样
无论输入内容长度如何,输出内容长度均一样
不可逆
很难被破解,据说现在存在被破解的可能,所以一般合约内采用两次加密的手段。
HASH 的应用
主要用于:生成加密后的唯一值
密码学 keccak256 和 encodePacked/encode
keccak256((bytes memory) returns (bytes32)
keccak256: 返回结果是 bytes32
这些编码函数可以用来构造函数调用数据,而不用实际进行调用。此外,keccak256(abi.encodePacked(a, b))
是一种计算结构化数据的哈希值(尽管我们也应该关注到:使用不同的函数参数类型也有可能会引起“哈希冲突” )的方式,不推荐使用的 keccak256(a, b)
。
Hash 算法在合约内使用
keccak256
进行。keccak256
返回值是bytes32
定长数据
可以使用
abi.encode
和abi.encodePacked
进行初步处理,然后传入keccak256
如果是多个参数,推荐使用
encode
;encodePacked
因为哈希碰撞,容易导致参数不同,结果相同encode
和encodePacked
的返回结果是 不定长的bytes
类型
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
function keccak256Test() public pure returns (bytes32 result) {
// ABC 的 keccak256 结果是:
// 0xe1629b9dda060bb30c7908346f6af189c16773fa148d3366701fbaa35d54f3c8
// keccak256 也被称作 sha3
return keccak256("ABC");
}
}
例子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Keccake256 {
function test1(string calldata _test1, string calldata _test2)
external
pure
returns (bytes32, bytes32)
{
return (
keccak256(encode(_test1, _test2)),
keccak256(encodePacked(_test1, _test2))
);
}
function encodePacked(string calldata _test1, string calldata _test2)
public
pure
returns (bytes memory)
{
return abi.encodePacked(_test1, _test2);
}
function encode(string calldata _test1, string calldata _test2)
public
pure
returns (bytes memory)
{
return abi.encode(_test1, _test2);
}
}
测试记录
/**
输入如下参数和返回结果
1.AA,BB
0:bytes32: 0x1edf4aae368e845d5d1cd28aec0624c467d538ecc7e5660765ed2afedca37aca
1:bytes32: 0xe5d11b08737f5dbf924278d835533b2b1e65c2fe1b5b119c5fdd21555547b9c4
2.AAA,BB
0:bytes32: 0x1db58b9736b1b30323e7dee1a1d1a71e8462dbd5651f55c364c3e9b8a3b28f10
1:bytes32: 0x741a09d43c38b2b6fc14dbc624b34865a62b9e8e13eb7a2f21263d0a1a11ed92
3.AA,ABB
0:bytes32: 0x40e845f25226ce557aee890fbb0084f9230ca5da5064f1922a15fd3941d7d423
1:bytes32: 0x741a09d43c38b2b6fc14dbc624b34865a62b9e8e13eb7a2f21263d0a1a11ed92
[AAA,BB] 和 [AA,ABB] 通过 encodePacked 得到的结果相同
*/
上面合约的改写
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Keccake256 {
function test1(string calldata _test1, string calldata _test2)
external
pure
returns (bytes32, bytes32)
{
return (
keccak256(encode(_test1, _test2)),
keccak256(encodePacked(_test1, 123, _test2))
);
}
function encodePacked(
string calldata _test1,
uint256 _x,
string calldata _test2
) public pure returns (bytes memory) {
return abi.encodePacked(_test1, _x, _test2);
}
function encode(string calldata _test1, string calldata _test2)
public
pure
returns (bytes memory)
{
return abi.encode(_test1, _test2);
}
}
密码学: sha256
sha256(bytes memory) returns (bytes32)
sha256: 返回结果是 bytes32
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
function sha256Test() public pure returns (bytes32 result) {
// ABC 的 sha256 结果是:
// 0xb5d4045c3f466fa91fe2cc6abe79232a1a57cdf104f7a26e716e0a1e2789df78
return sha256("ABC");
}
}
密码学: ripemd160
ripemd160(bytes memory) returns (bytes20)
ripemd160: 返回结果是 bytes20
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract Test {
function ripemd160Test() public pure returns (bytes20 result) {
// ABC 的 ripemd160 结果是:
// 0xdf62d400e51d3582d53c2d89cfeb6e10d32a3ca6
return ripemd160("ABC");
}
}
密码学: ecrecover
ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)
利用椭圆曲线签名恢复与公钥相关的地址,错误返回零值。
函数参数对应于 ECDSA 签名的值:
r = 签名的前 32 字节
s = 签名的第 2 个 32 字节
v = 签名的最后一个字节
ecrecover
返回一个 address, 而不是 address payable
。他们之前的转换参考 address payable
,如果需要转移资金到恢复的地址。参考案例
注意
如果你使用 ecrecover
,需要了解,在不需要知道相应的私钥下,签名也可以转换为另一个有效签名(可能是另外一个数据的签名)。在 Homestead 硬分叉,这个问题对于 transaction 签名已经解决了(查阅 EIP-2)。 不过 ecrecover
没有更改。
除非需要签名是唯一的,否则这通常不是问题,或者是用它们来识别物品。 OpenZeppelin 有一个 ECDSA 助手库 ,可以将其用作 ecrecover
的”包装“,而不会出现此问题。
在一个私链上,你很有可能碰到由于 sha256、ripemd160 或者 ecrecover 引起的 Out-of-Gas。这个原因就是他们被当做所谓的预编译合约而执行,并且在第一次收到消息后这些合约才真正存在(尽管合约代码是硬代码)。发送到不存在的合约的消息非常昂贵,所以实际的执行会导致 Out-of-Gas 错误。在你的合约中实际使用它们之前,给每个合约发送一点儿以太币,比如 1 Wei。这在官方网络或测试网络上不是问题。
ecrecover:
这个比较复杂,请在下面的 通过智能合约验证签名 例子详细查看;
案例 1:通过智能合约验证签名
本案例的关键字
keccak256
abi.encodePacked
ecrecover()
assembly
mload
add
案例解析
做一个 DEMO:链上对任意消息进行加密,加密消息在链下使用私钥再次加密,然后对再次加密的信息进行校验。
获取消息的 Hash 值
hash = msgHash(_message);
在【链下】将 hash 使用 MetaMask 进行私钥签名;
_signature = metaMaskSignHash(hash,addressPrivateKey)
这里相当于在 Metamask 对 hash 做第二次的 keccak256 Hash 转换,转换时添加了
"\x19Ethereum Signed Message:\n32"
使用
ecrecover
方法恢复签名地址ecrecoverAddress = recoverAds(hash,_signature)
这里可以恢复 MetaMask 签名时候使用的地址
校验签名结果是否正确 ecrecoverAddress == addressPublicKey ? “验证成功” : “验证失败”;
代码如下
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract VerifySig {
/**
功能:校验签名结果是否正确
注意 _signature 是 bytes 类型的
*/
function verify(
address addressPublicKey,
string calldata _message,
bytes calldata _signature
) external pure returns (bool) {
bytes32 hash = msgHash(_message);
// bytes32 _signature = metaMaskSignHash(hash,addressPrivateKey); // 这是在链下操作
address ecrecoverAddress = recoverAds(hash, _signature);
return ecrecoverAddress == addressPublicKey;
}
function msgHash(string calldata _message)
public
pure
returns (bytes32 keccakFirst)
{
keccakFirst = keccak256(abi.encodePacked(_message));
}
function msgHash2(bytes32 _msgHash)
internal
pure
returns (bytes32 keccakSecond)
{
// 两次2次签名,据说是数学层面上1次签名有被破解的可能。[我没有亲自验证过]
keccakSecond = keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", _msgHash)
);
}
function recoverAds(bytes32 _msgHash, bytes calldata _signature)
public
pure
returns (address)
{
// metamsk 签名会在原有消息上添加 "\x19Ethereum Signed Message:\n32",所以需要处理一下
bytes32 metamaskInputHash = msgHash2(_msgHash);
// r为点的x坐标,s为点的y坐标,v是坐标的奇偶检验标识符
// v是用于说明那个点才是真正符合结果的点
// https://www.cnblogs.com/wanghui-garcia/p/9662140.html
// https://www.jianshu.com/p/090f605f1842/
(bytes32 r, bytes32 s, uint8 v) = _split(_signature);
address ecrecoverAddress = ecrecover(metamaskInputHash, v, r, s);
return ecrecoverAddress;
}
function _split(bytes memory _signature)
internal
pure
returns (
bytes32 r,
bytes32 s,
uint8 v
)
{
// 需要内联汇编进行分割,合约没有别的方法
require(_signature.length == 65, "invalid signature length");
assembly {
r := mload(add(_signature, 32))
s := mload(add(_signature, 64))
// v := mload(add(_signature, 96))
// 因为 v 不是 bytes32,是 uint8数字,uint8数字只占1位,所以使用 byte(0)转换
v := byte(0, mload(add(_signature, 96)))
}
}
}
链下签名和 Remix 验证
使用浏览器控制台进行签名,需要安装 MateMask
// 1.打开 ethereum
ethereum.enable()
// 2.赋值地址。这里的地址是 MateMask 的默认地址
const address = "0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac";
// 3.赋值Hash。 使用 msgHash1 方法,输入 "ABC" 获取到的结果
const hash = "0xe1629b9dda060bb30c7908346f6af189c16773fa148d3366701fbaa35d54f3c8"
// 4. 呼起 MataMask 签名
ethereum.request({method:"personal_sign",params:[address,hash]});
// 5. 打开返回的 Promise {<pending>},拷贝 PromiseResult 值
0x66029be70a055a4abc293072c76550ffaecb2adb9fc3be2366d78bc498e008d06b6ddbfef97392a27a58737c33b059e09bb069261bdc41f9f0d8d1bc6e0b7ae31c
// 6. 在 recoverAds 中验证恢复的地址是否为签名地址。
上面的 PromiseResult 值是 _signature
上面的 hash 值是 _msgHash
// 7. 在 verify 中再次校验
扩展阅读: 在线进行签名的网站: https://metamask.github.io/test-dapp/
全局:ABI 编码及解码函数
ABI 全名 Application Binary Interface。ABI 用于底层调用的辅助使用;在合约调用合约的时候使用,可以不知道对方的合约源码,只需要知道链上逻辑即可。
ABI 编码
abi.encode(...) returns (bytes)
: :ref:ABI <ABI>
- 对给定参数进行编码abi.encodePacked(...) returns (bytes)
:对给定参数执行 :ref:紧打包编码 <abi_packed_mode>
,注意,可以不明确打包编码。abi.encodeWithSelector(bytes4 selector, ...) returns (bytes)
: :ref:ABI <ABI>
- 对给定第二个开始的参数进行编码,并以给定的函数选择器作为起始的 4 字节数据一起返回abi.encodeWithSignature(string signature, ...) returns (bytes)
:等价于abi.encodeWithSelector(bytes4(keccak256(signature), ...)
abi.encodeCall(function functionPointer, (...)) returns (bytes memory)
: 使用 tuple 类型参数 ABI 编码调用functionPointer
。执行完整的类型检查, 确保类型匹配函数签名。结果和abi.encodeWithSelector(functionPointer.selector, (...))
一致。
ABI 解码
abi.decode(bytes memory encodedData, (...)) returns (...)
: 对给定的数据进行 ABI 解码,而数据的类型在括号中第二个参数给出 。 例如:(uint a, uint[2] memory b, bytes memory c) = abi.decode(data, (uint, uint[2], bytes))
encode
encode
会补零
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract AbiDecode {
function encode(string memory a, string memory b)
external
pure
returns (bytes memory)
{
return abi.encode(a, b);
}
}
/**
/**
输入如下参数和返回结果
1.AA,BB
0x
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000080
0000000000000000000000000000000000000000000000000000000000000002
4141000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000002
4242000000000000000000000000000000000000000000000000000000000000
2.AAA,BB
0x
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000080
0000000000000000000000000000000000000000000000000000000000000003
4141410000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000002
4242000000000000000000000000000000000000000000000000000000000000
3.AA,ABB
0x
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000080
0000000000000000000000000000000000000000000000000000000000000002
4141000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000003
4142420000000000000000000000000000000000000000000000000000000000
*/
*/
encodePacked
encodePacked
不会补零不补零,容易导致碰撞错误。(两个参数拼在一起,导致参数不同,结果相同)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract AbiDecode {
function encodePacked(string memory a, string memory b)
external
pure
returns (bytes memory)
{
return abi.encodePacked(a, b);
}
}
/**
输入如下参数和返回结果
1.AA,BB
0x41414242
2.AAA,BB
0x4141414242
3.AA,ABB
0x4141414242
[AAA,BB] 和 [AA,ABB] 得到的结果相同
*/
解决 encodePacked
的哈希碰撞问题
可以在要编码的数据中间加一个固定的值,如果
代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract AbiDecode {
function encodePacked(string calldata _test1, string calldata _test2)
public
pure
returns (bytes memory)
{
uint256 x = 123;
return abi.encodePacked(_test1, x, _test2);
}
}
结果如下:
/**
输入如下参数和返回结果
1.AA,BB
0x4141000000000000000000000000000000000000000000000000000000000000007b4242
2.AAA,BB
0x414141000000000000000000000000000000000000000000000000000000000000007b4242
3.AA,ABB
0x4141000000000000000000000000000000000000000000000000000000000000007b414242
[AAA,BB] 和 [AA,ABB] 因为间隔了数据,所以得到的结果不相同
*/
注意
这些编码函数可以用来构造函数调用数据,而不用实际进行调用。此外,keccak256(abi.encodePacked(a, b))
是一种计算结构化数据的哈希值(尽管我们也应该关注到:使用不同的函数参数类型也有可能会引起“哈希冲突” )的方式,不推荐使用的 keccak256(a, b)
。
decode
例子 1
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract AbiDecode {
struct MyStruct {
string name;
uint256[2] nums;
}
function encode(
uint256 x,
address addr,
uint256[] calldata arr,
MyStruct calldata myStruct
) external pure returns (bytes memory) {
return abi.encode(x, addr, arr, myStruct);
}
function decode(bytes calldata data)
external
pure
returns (
uint256 x,
address addr,
uint256[] memory arr,
MyStruct memory myStruct
)
{
(x, addr, arr, myStruct) = abi.decode(
data,
(uint256, address, uint256[], MyStruct)
);
}
}
合约测试
部署
encode
参数如下:
1
0xffD0d80c48F6C3C5387b7cfA7AA03970bdB926ac
[1,2,3]
["Anbang",[2,3]]
得到的结果,进行
decode
abi.encodeWithSelector
这是获取函数签名使用的,第一个参数为函数选择,如下是第二章在介绍地址类型的时候,staticcall 静态调用 用法的参数,需要由 abi.encodeWithSelector
计算出来。函数的参数按照顺序写在函数名之后即可。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
// 被调用的合约
contract Hello {
function echo() external pure returns (string memory) {
return "Hello World!";
}
}
// 调用者合约
contract SoldityTest {
function callHello(address _ads) external view returns (string memory) {
// 编码被调用者的方法签名
bytes4 methodId = bytes4(keccak256("echo()"));
// 调用合约
(bool success, bytes memory data) = _ads.staticcall(
abi.encodeWithSelector(methodId)
);
if (success) {
return abi.decode(data, (string));
} else {
return "error";
}
}
}
abi.encodeWithSignature
这是获取函数签名使用的,第一个参数为函数的名字和参数类型,如下是第二章在介绍 地址类型的时候,call 用法的参数,需要由 abi.encodeWithSignature
计算出来。函数的参数按照顺序写在函数名之后即可。
function call_Test1_setNameAndAge(
address _ads,
string memory _name,
uint256 _age
) external payable {
bytes memory data = abi.encodeWithSignature(
"setNameAndAge(string,uint256)",
_name,
_age
);
(bool success, bytes memory _bys) = _ads.call{value: msg.value}(data);
require(success, "Call Failed");
bys = _bys;
}
abi.encodeCall
补充:函数赋值给变量 & 函数作为参数 & 函数中返回函数
可以将一个函数赋值给另一个函数类型的变量,也可以将一个函数作为参数进行传递,还能在函数调用中返回函数类型变量。
可以将一个函数赋值给一个变量,一个函数类型的变量。
还可以将一个函数作为参数进行传递。
也可以在函数调用中返回一个函数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract FnTest1 {
function internalFunc() internal pure returns (uint256) {
return 1;
}
function externalFunc() external pure returns (uint256) {
return 2;
}
function callFunc() public view returns (uint256, uint256) {
//直接使用内部的方式调用
uint256 a = internalFunc();
//不能在内部调用一个外部函数,会报编译错误。
// externalFunc();
//使用`this`以`external`的方式调用一个外部函数
uint256 b = this.externalFunc();
//不能通过`external`的方式调用一个`internal`
//this.internalFunc();
return (a, b);
}
}
contract FnTest2 {
function externalCall(FnTest1 ft) public pure returns (uint256) {
//调用另一个合约的外部函数
uint256 a = ft.externalFunc();
//不能调用另一个合约的内部函数
//ft.internalFunc();
return a;
}
}
具名调用和匿名函数参数
函数调用参数也可以按照任意顺序由名称给出,如果它们被包含在 { }
中, 如以下示例中所示。参数列表必须按名称与函数声明中的参数列表相符,但可以按任意顺序排列。
pragma solidity >=0.4.0 <0.9.0;
contract C {
mapping(uint => uint) data;
function f() public {
set({value: 2, key: 3});
}
function set(uint key, uint value) public {
data[key] = value;
}
}
省略函数参数名称
未使用参数的名称(特别是返回参数)可以省略。这些参数仍然存在于堆栈中,但它们无法访问。
pragma solidity >=0.4.22 <0.9.0;
contract C {
// 省略参数名称
function func(uint k, uint) public pure returns(uint) {
return k;
}
}
调用异常
如果当函数类型的变量还没有初始化时就调用它的话会引发一个 Panic
异常。 如果在一个函数被 delete
之后调用它也会发生相同的情况。
外部函数类型在 Solidity 的上下文环境以外的地方使用
如果外部函数类型在 Solidity 的上下文环境以外的地方使用,它们会被视为 function
类型。 该类型将函数地址紧跟其函数标识一起编码为一个 bytes24 类型。。
实战应用
权限控制合约
权限控制的核心是: mapping
默认给部署者赋
ADMIN
权限ADMIN 和 USER 使用 bytes32,并且 private
只有 ADMIN 权限才可以添加和撤销权限
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract AccessControl {
// 状态变量
mapping(bytes32 => mapping(address => bool)) public roles;
// ADMIN 和 private 是私有变量,可以先改为 public;获取到值后,再改为 private
// 0xdf8b4c520ffe197c5343c6f5aec59570151ef9a492f2c624fd45ddde6135ec42
bytes32 private constant ADMIN = keccak256(abi.encodePacked("ADMIN"));
// 0x2db9fd3d099848027c2383d0a083396f6c41510d7acfd92adc99b6cffcf31e96
bytes32 private constant USER = keccak256(abi.encodePacked("USER"));
// 事件
event GrantRole(address indexed ads, bytes32 indexed role);
event RevokeRole(address indexed ads, bytes32 indexed role);
// 函数修改器
modifier onlyRole(bytes32 _role) {
require(roles[_role][msg.sender], "Not Authorized");
_;
}
// 构造函数
constructor() {
_grantRole(msg.sender, ADMIN);
}
// 函数
function _grantRole(address _ads, bytes32 _role) internal {
roles[_role][_ads] = true;
emit GrantRole(_ads, _role);
}
function grantRole(address _ads, bytes32 _role) external onlyRole(ADMIN) {
_grantRole(_ads, _role);
}
function revokeRole(address _ads, bytes32 _role) external onlyRole(ADMIN) {
roles[_role][_ads] = false;
emit RevokeRole(_ads, _role);
}
}
问答题
如下合约中,test 返回什么?
// SPDX-License-Identifier: MIT pragma solidity ^0.8.18; contract Demo { //当给返回值赋值后,并且有个return,以最后的return为主 function test() public pure returns (uint256 mul) { uint256 a = 10; mul = 100; return a; } }
函数参数使用时候有哪些需要注意的?
引用类型需要
memory
/calldata
函数参数可以当作为本地变量,也可用在等号左边被赋值。
外部函数不支持多维数组,如果原文件加入 p
ragma abicoder v2;
可以启用 ABI v2 版编码功能,这此功能可用。
创建一个
Utils
合约,其中有sum
方法,传入任意数量的数组,都可以计算出求和结果。函数既可以定义在合约内部,也可以定义在合约外部,两种方式的区别是什么?
合约之外的函数(也称为“自由函数”)始终具有隐式的
internal
可见性。 它们的代码包含在所有调用它们合约中,类似于内部库函数。在合约之外定义的函数仍然在合约的上下文内执行。他们仍然可以访问变量
this
,也可以调用其他合约,将其发送以太币或销毁调用它们合约等其他事情。与在合约中定义的函数的主要区别为:自由函数不能直接访问存储变量和不在他们的作用域范围内函数。
函数的构造函数有什么特点?
它仅能在智能合约部署的时候调用一次,创建之后就不能再次被调用。
构造函数是可选的,只允许有一个构造函数,这意味着不支持重载。(普通函数支持重载)
:在合约创建的过程中,它的代码还是空的,所以直到构造函数执行结束,我们都不应该在其中调用合约自己的函数。(可以直接写函数名调用,但是不推荐调用,不可以通过 this 来调用函数,因为此时真实的合约实例还没有被创建。)
构造函数有哪些用途?
用来设置管理账号,Token 信息等可以自定义,并且以后永远不需要修改的数据。
可以用来做初识的权限设置,避免后续没办法 owner/admin 地址。
合约内调用外部有哪些?
也可以使用表达式
this.g(8)
; 和c.g(2)
; 进行调用,其中 c 是合约实例, g 合约内实现的函数,这两种方式调用函数,称为“外部调用”,它是通过消息调用来进行,而不是直接的代码跳转。请注意,不可以在构造函数中通过this
来调用函数,因为此时真实的合约实例还没有被创建。
从一个合约到另一个合约的函数调用会创建交易么?
从一个合约到另一个合约的函数调用不会创建自己的交易, 它是作为整个交易的一部分的消息调用。
调用函数并转帐如何实现
feed.info{value: 10, gas: 800}(2);
注意
feed.info{value: 10, gas: 800}
仅(局部地)设置了与函数调用一起发送的 Wei 值和 gas 的数量,只有最后的小括号才执行了真正的调用。 因此,feed.info{value: 10, gas: 800}
是没有调用函数的,value
和gas
设置是无效的。
extcodesize 操作码会检查要调用的合约是否确实存在,有哪些特殊情况?
低级 call 调用,会绕过检查
预编译合约的时候,也会绕过检查。
与其他和月交互时候有什么需要注意的?
任何与其他合约的交互都会产生潜在危险,尤其是在不能预先知道合约代码的情况下。
小心这个交互调用在返回之前再回调我们的合约,这意味着被调用合约可以通过它自己的函数改变调用合约的状态变量。 一个建议的函数写法是,例如,在合约中状态变量进行各种变化后再调用外部函数,这样,你的合约就不会轻易被滥用的重入攻击 (reentrancy) 所影响
public 既可以被当作内部函数也可以被当作外部函数。使用时候有什么注意的?
如果想将一个函数当作内部函数使用,就用
f
调用,如果想将其当作外部函数,使用this.f
。
pure 函数中,哪些行为被视为读取状态。
读取状态变量。
这也意味着读取
immutable
变量也不是一个pure
操作。
访问
address(this).balance
或<address>.balance
访问
block
,tx
,msg
中任意成员 (除msg.sig
和msg.data
之外)。调用任何未标记为
pure
的函数。使用包含特定操作码的内联汇编。
TODO:
这个不了解,需要用例子加深印象。
使用操作码
STATICCALL
, 这并不保证状态未被读取, 但至少不被修改。
pure 函数发生错误时候,有什么需要注意的?
如果发生错误,
pure
函数可以使用revert()
和require()
函数来还原潜在的状态更改。还原状态更改不被视为 状态修改, 因为它只还原以前在没有view
或pure
限制的代码中所做的状态更改, 并且代码可以选择捕获 revert 并不传递还原。这种行为也符合 STATICCALL 操作码。
view 函数中,哪些行为视为修改状态。
修改状态变量。
触发事件。
创建其它合约。
使用
selfdestruct
。通过调用发送以太币。
调用任何没有标记为 view 或者 pure 的函数。
使用底层调用
(TODO:这里是 call 操作么?)
使用包含某些操作码的内联程序集。
pure/view/payable/这些状态可变性的类型转换是怎么样的?
pure 函数可以转换为 view 和 non-payable 函数
view 函数可以转换为 non-payable 函数
payable 函数可以转换为 non-payable 函数
其他的转换则不可以。
使用 return 时,有哪些需要注意的?
函数返回类型不能为空 —— 如果函数类型不需要返回,则需要删除整个
returns (<return types>)
部分。函数可能返回任意数量的参数作为输出。函数的返回值有两个关键字,一个是
returns
,一个是return
;returns
是在函数名后面的,用来标示返回值的数量,类型,名字信息。return
是在函数主体内,用于返回returns
指定的数据信息
如果使用 return 提前退出有返回值的函数, 必须在用 return 时提供返回值。
非内部函数有些类型没法返回,比如限制的类型有:多维动态数组、结构体等。
解构赋值一个函数返回多值时候,元素数量必须一样。
函数的签名的逻辑是什么?为什么函数可以重载?
核心:
bytes4(keccak256(bytes("transfer(address,uint256)")))
函数签名被定义为基础原型的规范表达,而基础原型是函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格。
函数重载需要怎么样实现?
这些相同函数名的函数,参数(参数类型或参数数量)必须不一样。,因为只有这样才能签出来不同的函数选择器。
如果两个外部可见函数仅区别于 Solidity 内的类型,而不是它们的外部类型则会导致错误。很难理解,需要看例子。
函数重载的参数匹配原理
通过将当前范围内的函数声明与函数调用中提供的参数相匹配,这样就可以选择重载函数。
如果所有参数都可以隐式地转换为预期类型,则该函数作为重载候选项。如果一个匹配的都没有,解析失败。
返回参数不作为重载解析的依据。
function f(uint8 val) public pure returns (uint8 out)
和function f(uint256 val) public pure returns (uint256 out)
是合法的函数重载么?不是的。
在 Remix 里,部署 A 合约,会将两个方法都渲染出来,调用
f(50)
/f(256)
都可以。但是实际调用里,在其他合约内调用
f(50)
会导致类型错误,因为50
既可以被隐式转换为uint8
也可以被隐式转换为uint256
。 另一方面,调用f(256)
则会解析为f(uint256)
重载,因为256
不能隐式转换为uint8
。
函数修改器的意义是什么?有什么作用?
意义:我们可以将一些通用的操作提取出来,包装为函数修改器,来提高代码的复用性,改善编码效率。是函数高内聚,低耦合的延伸。
作用:
modifier
常用于在函数执行前检查某种前置条件。比如地址对不对,余额是否充足,参数值是否允许等
修改器内可以写逻辑
特点:
modifier
是一种合约属性,可被继承,同时还可被派生的合约重写(override)。(修改器 modifier 是合约的可继承属性,并可能被派生合约覆盖 , 但前提是它们被标记为 virtual)。_
符号可以在修改器中出现多次,每处都会替换为函数体。
Solidity 有哪些全局的数学和密码学函数?
数学函数:
addmod(uint x, uint y, uint k) returns (uint)
计算
(x + y) % k
,加法会在任意精度下执行,并且加法的结果即使超过2**256
也不会被截取。从 0.5.0 版本的编译器开始会加入对k != 0
的校验(assert)。
mulmod(uint x, uint y, uint k) returns (uint)
计算
(x * y) % k
,乘法会在任意精度下执行,并且乘法的结果即使超过2**256
也不会被截取。从 0.5.0 版本的编译器开始会加入对k != 0
的校验(assert)。
密码学函数:
keccak256((bytes memory) returns (bytes32)
计算 Keccak-256 哈希,之前 keccak256 的别名函数 sha3 在 0.5.0 中已经移除。。
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)
利用椭圆曲线签名恢复与公钥相关的地址,错误返回零值。
函数参数对应于 ECDSA 签名的值:
r = 签名的前 32 字节
s = 签名的第 2 个 32 字节
v = 签名的最后一个字节
ecrecover 返回一个 address, 而不是 address payable。