目录


全局变量(block.timestamp等)
描述区块链信息的全局变量,包括:
区块和交易的属性
函数 | 含义 | 备注 |
---|---|---|
blockhash(uint blockNumber) | (byte32)哈希值 | |
block.coinbase | (address) 当前块矿工的地址。 | |
block.difficulty | (uint)当前块的难度 | |
block.gaslimit | (uint)当前块的gaslimit | |
block.number | (uint)当前区块的块号 | |
block.timestamp | (uint)当前块的时间戳 | 常用 |
msg.data | (bytes)完整的调用数据(calldata) | 常用 |
gasleft() | (uint)当前还剩的gas | |
msg.sender | (address)当前调用发起人的地址 | 常用 |
msg.sig | (bytes4)调用数据的前四个字节(函数标识符) | 常用 |
msg.value | (uint)这个消息所附带的货币量,单位为wei | 常用 |
block.timestamp | (uint) 当前时间戳 | 常用 |
tx.gasprice | (uint) 交易的gas价格 | |
tx.origin | (address)交易的发送者的地址(EOA) | 常用 |
msg.sender、msg.value、msg.data
当用户发起一笔交易时,相当于向合约发送一个消息(msg),这笔交易可能会涉及到三个重要的全局变量,具体如下:
- msg.sender:表示这笔交易的调用者是谁(地址),同一个交易,不同的用户调用,msg.sender不同;
- msg.value:表示调用这笔交易时,携带的ether数量,这些以太坊由msg.sender支付,转入到当前合约(wei单位整数);
- 注意:一个函数(或地址)如果想接收ether,需要将其修饰为:payable。
- msg.data:表示调用这笔交易的信息,由函数签名和函数参数(16进制字符串),组成代理模式时常用msg.data(后续讲解)。
msg.sender
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract MsgSender {
address public owner;
uint256 public value;
address public caller;
constructor() {
//在部署合约的时候,设置一个全局唯一的合约所有者,后面可以使用权限控制
owner = msg.sender;
}
//1. 对与合约而言,msg.sender是一个可以改变的值,并不一定是合约的创造者
//2. 任何人调用了合约的方法,那么这笔交易中的from就是当前合约中的msg.sender
function setValue(uint256 input) public {
value = input;
caller = msg.sender;
}
}
msg.value
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract MsgValue {
// uint256 public money;
mapping(address=> uint256) public personToMoney;
// 函数里面使用了msg.value,那么函数要修饰为payable
function play() public payable {
// 如果转账不是100wei,那么参与失败
// 否则成功,并且添加到维护的mapping中
require(msg.value == 100, "should equal to 100!");
personToMoney[msg.sender] = msg.value;
}
// 查询当前合约的余额
function getBalance() public view returns(uint256) {
return address(this).balance;
}
}
msg.data
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract MsgData {
event Data(bytes data, bytes4 sig);
// input0: addr: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
// input1: amt : 1
function transfer(address addr, uint256 amt) public {
bytes memory data = msg.data;
// msg.sig 表示当前方法函数签名(4字节)
// msg.sig 等价于 this.transfer.selector
emit Data(data, msg.sig);
}
//output:
// - data: 0xa9059cbb0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc40000000000000000000000000000000000000000000000000000000000000001
// - sig: 0xa9059cbb
// 对data进行分析:
// 0xa9059cbb //前四字节
// 0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4 //第一个参数占位符(32字节)
// 0000000000000000000000000000000000000000000000000000000000000001 //第二个参数占位符(32字节)
}
payable
- 一个函数(或地址)如果想接收ether,需要将其修饰为:payable。
- address常用方法:
- balance(): 查询当前地址的ether余额
- transfer(uint): 合约向当前地址转指定数量的ether,如果失败会回滚
- send(uint): 合约向当前地址转指定数量的ether,如果失败会返回false,不回滚(不建议使用send)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Payable {
// 1. Payable address can receive Ether
address payable public owner;
// 2. Payable constructor can receive Ether
constructor() payable {
owner = payable(msg.sender);
}
// 3. Function to deposit Ether into this contract.
function deposit() public payable {}
// 4. Call this function along with some Ether.
// The function will throw an error since this function is not payable.
function notPayable() public {}
// 5. Function to withdraw all Ether from this contract.
function withdraw() public {
uint amount = address(this).balance;
owner.transfer(amount);
}
// 6. Function to transfer Ether from this contract to address from input
function transfer(address payable _to, uint _amount) public {
_to.transfer(_amount);
}
}
Try on Remix
abi.encode、abi.decode、abi.encodePacked
abi.encode:可以将data编码成bytes,生成的bytes总是32字节的倍数,不足32为会自动填充(用于给合约调用);
abi.decode:可以将bytes解码成data(可以只解析部分字段)
abi.encodePacked:与abi.encode类似,但是生成的bytes是压缩过的(有些类型不会自动填充,无法传递给合约调用)。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract AbiDecode {
struct MyStruct {
string name;
uint[2] nums;
}
// input: 10, 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, [1,"2",3], ["duke", [10,20]]
// output: 0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc40000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000064756b6500000000000000000000000000000000000000000000000000000000
// output长度:832位16进制字符(去除0x),832 / 32 = 26 (一定是32字节的整数倍,不足填0)
function encode(
uint x,
address addr,
uint[] 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 (
uint x,
address addr,
uint[] memory arr,
MyStruct memory myStruct
)
{
(x, addr, arr, myStruct) = abi.decode(data, (uint, address, uint[], MyStruct));
/* decode output:
0: uint256: x 10
1: address: addr 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
2: uint256[]: arr 1,2,3
3: tuple(string,uint256[2]): myStruct ,10,20
*/
}
// 可以只decode其中部分字段,而不用全部decode,当前案例中,只有第一个字段被解析了,其余为默认值
function decodeLess(bytes calldata data)
external
pure
returns (
uint x,
address addr,
uint[] memory arr,
MyStruct memory myStruct
)
{
(x) = abi.decode(data, (uint));
/* decode output:
0: uint256: x 10
1: address: addr 0x0000000000000000000000000000000000000000
2: uint256[]: arr
3: tuple(string,uint256[2]): myStruct ,0,0
*/
}
// input: -1, 0x42, 0x03, "Hello, world!"
function encodePacked(
int16 x,
bytes1 y,
uint16 z,
string memory s
) external view returns (bytes memory) {
// encodePacked 不支持struct和mapping
return abi.encodePacked(x, y, z, s);
/*
0xffff42000348656c6c6f2c20776f726c6421
^^^^ int16(-1)
^^ bytes1(0x42)
^^^^ uint16(0x03)
^^^^^^^^^^^^^^^^^^^^^^^^^^ string("Hello, world!") without a length field
*/
}
}
使用三方库:
web3js编码:
// encodeFunctionCall( abi ,参数 ) 得到编码
web3.eth.abi.encodeFunctionCall({
name: 'myMethod',
type: 'function',
inputs: [{
type: 'uint256',
name: 'myNumber'
},{
type: 'string',
name: 'myString'
}]
}, ['2345675643', 'Hello!%']);
> "0x24ee0097000000000000000000000000000000000000000000000000000000008bd02b7b0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000748656c6c6f212500000000000000000000000000000000000000000000000000"
web3js解码:
解码时,要将得到的calldata的前四字节去掉,那是函数的selector,不应该参与参数解析。
// let res = web3.eth.abi.decodeParameters(abi, calldata)
async function main() {
let calldata = '' //太大了,删除,去下面线上链接中获取
let abi = [{ "internalType": "string", "name": "_id", "type": "string" }, { "internalType": "string", "name": "_uniqueId", "type": "string" }, { "internalType": "uint8", "name": "_assetFrom", "type": "uint8" }, { "internalType": "uint8", "name": "_action", "type": "uint8" }, { "internalType": "address", "name": "_srcToken", "type": "address" }, { "internalType": "address", "name": "_dstToken", "type": "address" }, { "internalType": "uint256", "name": "_srcAmount", "type": "uint256" }, { "internalType": "uint256", "name": "_srcFeeAmount", "type": "uint256" }, { "internalType": "bytes", "name": "_data", "type": "bytes" }]
let res = web3.eth.abi.decodeParameters(abi, calldata)
console.log('res:', res)
}
在线案例:https://web3playground.io/QmSeHtJPLFxweiGB8ocFDXkCZao6Mt5oEJ4Ej66iY3RL1R
ethersjs
let bytes2 = mock1Inch.interface.encodeFunctionData("swap", [ETH_ADDR, daiToken.address, 90])
call
call是一种底层调用合约的方式,可以在合约内调用其他合约,call语法为:
//(bool success, bytes memory data) = addr.call{value: valueAmt, gas: gasAmt}(abi.encodeWithSignature("foo(string,uint256)", 参数1, 参数2)
其中:
1. success:执行结果,一定要校验success是否成功,失败务必要回滚
2. data:执行调用的返回值,是打包的字节序,需要解析才能得到调用函数的返回值(后续encode_decode详解)
当调用fallback方式给合约转ether的时候,建议使用call,而不是使用transfer或send方法
(bool success, bytes memory data) = addr.call{value: 10}("")
对于存在的方法,不建议使用call方式调用。
(bool success, bytes memory data) = _addr.call(abi.encodeWithSignature("doesNotExist()"));
调用不存在的方法(又不存在fallback)时,交易会调用成功,但是第一个参数为:false。
完整demo:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Receiver {
event Received(address caller, uint amount, string message);
fallback() external payable {
emit Received(msg.sender, msg.value, "Fallback was called");
}
function foo(string memory _message, uint _x) public payable returns (uint) {
emit Received(msg.sender, msg.value, _message);
return _x + 1;
}
}
contract Caller {
event Response(bool success, bytes data);
function testCallFoo(address payable _addr) public payable {
// You can send ether and specify a custom gas amount
(bool success, bytes memory data) = _addr.call{value: msg.value, gas: 5000}(
abi.encodeWithSignature("foo(string,uint256)", "call foo", 123)
);
emit Response(success, data);
}
// Calling a function that does not exist triggers the fallback function.
function testCallDoesNotExist(address _addr) public {
(bool success, bytes memory data) = _addr.call(
abi.encodeWithSignature("doesNotExist()")
);
emit Response(success, data);
}
}
Try on Remix
keccak256哈希算法
keccak256用于计算哈希,属于sha3算法,与sha256(属于sha2算法不同),keccak256使用场景如下:
- 用于生成唯一id;
- 生成数据指纹;
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract HashFunction {
function hash(
string memory _text,
uint _num,
address _addr
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_text, _num, _addr));
}
// Example of hash collision
// Hash collision can occur when you pass more than one dynamic data type
// to abi.encodePacked. In such case, you should use abi.encode instead.
function collision(string memory _text, string memory _anotherText)
public
pure
returns (bytes32)
{
// encodePacked(AAA, BBB) -> AAABBB
// encodePacked(AA, ABBB) -> AAABBB
return keccak256(abi.encodePacked(_text, _anotherText));
}
}
contract GuessTheMagicWord {
bytes32 public answer =
0x60298f78cc0b47170ba79c10aa3851d7648bd96f2f8e46a19dbc777c36fb0c00;
// Magic word is "Solidity"
function guess(string memory _word) public view returns (bool) {
return keccak256(abi.encodePacked(_word)) == answer;
}
}
Try on Remix
selector
- 当调用某个function时,具体调用function的信息会被拼装成calldata,calldata的前4个字节就是这个function的selector,其中:
- 通过selector可以知道调用的是哪个function;
- calldata 可以通过msg.data获取。
- 通过拼装selector和函数参数,我们可以在A合约中得到calldata,并在A合约中通过call方法去调用B合约中的方法,从而实现合约间的调用。举例,下面的代码功能是:在当前合约中使用call调用addr地址中的transfer方法:
// 在合约中,一个function的4字节selector可以通过abi.encodeWithSignature(...)来获取
// "0xa9059cbb"
bytes memory transferSelector = abi.encodeWithSignature("transfer(address,uint256)");
// 调用合约
addr.call(transferSelector, 0xSomeAddress, 100);
// 一般会写成一行
// addr.call(abi.encodeWithSignature("transfer(address,uint256)"), 0xSomeAddress, 100);
- 另外一种计算selector的方式为:(keccak256即sha3哈希算法)
bytes4(keccak256(bytes("transfer(address,uint256)"))) //不用关心返回值,不用放在这里面计算。
- 在合约内部也可以直接获取selector
// 假设当前合约内有transfer函数
this.transfer.selector
- 也可以使用abi.encodeCall方式获取
interface IERC20 {
function transfer(address, uint) external;
}
function encodeCall(address to, uint amount) external pure returns (bytes memory) {
// Typo and type errors will not compile
return abi.encodeCall(IERC20.transfer, (to, amount));
}
完整demo:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
interface IERC20 {
function transfer(address, uint) external;
}
contract FunctionSelector {
event Selector(bytes4 s1, bytes4 s2);
//_func示例值: "transfer(address,uint256)"
function getSelector(string calldata _func) external pure returns (bytes4, bytes memory) {
bytes4 selector1 = bytes4(keccak256(bytes(_func)));
bytes memory selector2 = abi.encodeWithSignature(_func);
// 两者相同
// 0: bytes4: 0xa9059cbb
// 1: bytes: 0xa9059cbb
return (selector1, selector2);
}
function encodeWithSignature(address to, uint amount)
external
pure
returns (bytes memory)
{
// Typo is not checked - "transfer(address, uint)",不会检查参数类型
return abi.encodeWithSignature("transfer(address,uint256)", to, amount);
}
function encodeWithSelector(address to, uint amount)
external
pure
returns (bytes memory)
{
// Type is not checked - (IERC20.transfer.selector, true, amount) ,不会检查to, amount类型
return abi.encodeWithSelector(IERC20.transfer.selector, to, amount);
}
function encodeCall(address to, uint amount) external pure returns (bytes memory) {
// Typo and type errors will not compile,校验最严格
return abi.encodeCall(IERC20.transfer, (to, amount));
}
}
补充知识点
- 一般提到signature的方法指的是函数原型:
"transfer(address,uint256)"
- 一般提到selector指的是前4字节
// bytes4(keccak256(bytes("transfer(address,uint256)")))
"Function sig:" "0xa9059cbb"
- 链下方式计算selector(详见在web3.js和ether.js章节),点击执行demo
async function main() {
let transferEvent = "Transfer(address,address,uint256)"
let sig1 = web3.eth.abi.encodeEventSignature(transferEvent)
let sig2 = web3.eth.abi.encodeFunctionSignature(transferEvent)
// 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
console.log('event sig1:', sig1)
// 0xddf252ad
console.log('event sig2:', sig2)
let transferFun = "transfer(address,uint256)"
let sig3 = web3.eth.abi.encodeFunctionSignature(transferFun)
// 0xa9059cbb,差一个字母千差万别
console.log('Function sig:', sig3)
}
- 哈希算法:
// keccak256与sha3和算法相同 ==》 brew install sha3sum
// sha256属于sha2系列(与sha3不同)。
Try on Remix
Send Ether(transfer、send、call)
https://docs.soliditylang.org/en/latest/security-considerations.html#sending-and-receiving-ether
如何发送ether?
有三种方式可以向合约地址转ether:
- transfer(2300 gas, throw error)
- send(2300 gas,return bool)
- call(传递交易剩余的gas或设置gas,不限定2300gas,return bool)(推荐使用)
如何接收ether?
想接收ether的合约至少包含以下方法中的一个:
- receive() external payable:msg.data为空时调用
- fallback() external payable:msg.data非空时调用
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract ReceiveEther {
/*
Which function is called, fallback() or receive()?
sender ether
|
msg.data is empty?
/ \
yes no
/ \
receive() exist? fallback()
/ \
yes no
/ \
receive() fallback()
*/
string public message;
// Function to receive Ether. msg.data must be empty
receive() external payable {
message = "receive called!";
}
// Fallback function is called when msg.data is not empty
fallback() external payable {
message = "fallback called!";
}
function getBalance() public view returns (uint) {
return address(this).balance;
}
function setMsg(string memory _msg) public {
message = _msg;
}
}
contract SendEther {
function sendViaTransfer(address payable _to) public payable {
// This function is no longer recommended for sending Ether. (不建议使用)
_to.transfer(msg.value);
}
function sendViaSend(address payable _to) public payable {
// Send returns a boolean value indicating success or failure.
// This function is not recommended for sending Ether. (不建议使用)
bool sent = _to.send(msg.value);
require(sent, "Failed to send Ether");
}
function sendViaCallFallback(address payable _to) public payable {
// Call returns a boolean value indicating success or failure.
// This is the current recommended method to use. (推荐使用)
(bool sent, bytes memory data) = _to.call{value: msg.value}(abi.encodeWithSignature("noExistFuncTest()"));
require(sent, "Failed to send Ether");
}
function sendViaCallReceive(address payable _to) public payable {
// Call returns a boolean value indicating success or failure.
// This is the current recommended method to use.(推荐使用)
(bool sent, bytes memory data) = _to.call{value: msg.value}("");
require(sent, "Failed to send Ether");
}
}
Try on Remix
delegatecall
delegatecall与call相似,也是底层调用合约方式,特点是:
- 当A合约使用delegatecall调用B合约的方法时,B合约的代码被执行,但是使用的是A合约的上下文,包括A合约的状态变量,msg.sender,msg.value等;
- 使用delegatecall的前提是:A合约和B合约有相同的状态变量。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract B {
// NOTE: storage layout must be the same as contract A
uint public num;
address public sender;
uint public value;
function setVars(uint _num) public payable {
num = _num;
sender = msg.sender;
value = msg.value;
}
}
// 注意:执行后,sender值为EOA的地址,而不是A合约的地址 (调用链EOA-> A::setVars -> B::setVars)
contract A {
uint public num;
address public sender;
uint public value;
function setVars(address _contract, uint _num) public payable {
// A's storage is set, B is not modified.
(bool success, bytes memory data) = _contract.delegatecall(
abi.encodeWithSignature("setVars(uint256)", _num)
);
}
}
Try on Remix
fallback
- fallback是特殊的函数,无参数,无返回值;
- 何时会被调用:
- 当被调用的方法不存在时,fallback会被调用,属于default函数;
- 当向合约转ether但是合约不存在receive函数时;
- 当向合约转ether但是msg.data不为空时。(即使receive存在)
- 当使用transfer或者send对合约进行转账时,fallback函数的gaslimit限定为2300 gas
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Fallback {
event Log(uint gas);
// Fallback function must be declared as external.
fallback() external payable {
// send / transfer (forwards 2300 gas to this fallback function)
// call (forwards all of the gas)
emit Log(gasleft());
}
// Helper function to check the balance of this contract
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
contract SendToFallback {
function transferToFallback(address payable _to) public payable {
// Log event: "gas": "2254"
_to.transfer(msg.value);
}
function callFallback(address payable _to) public payable {
// Log event: "gas": "6110"
(bool sent, ) = _to.call{value: msg.value}("");
require(sent, "Failed to send Ether");
}
function callNoExistFunc(address payable _to) public payable {
// call no exist funtion will call fallback by default
// Log event: "gas": "5146"
(bool sent, ) = _to.call{value: msg.value}(abi.encodeWithSignature("noExistFunc()"));
require(sent, "Failed to call");
}
}
Try on Remix
合约间调用
普通的交易,相当于在世界状态中修改原有的账户数据,更新到新状态。
一共有三种方式调用合约:
- 使用合约实例调用合约(常规):A.foo(argument)
- 使用call调用合约: A.call(calldata)
- 使用delegate调用合约:A.delegatecall(calldata)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Callee {
uint public x;
uint public value;
function setX(uint _x) public returns (uint) {
x = _x;
return x;
}
function setXandSendEther(uint _x) public payable returns (uint, uint) {
x = _x;
value = msg.value;
return (x, value);
}
}
contract Caller {
// 直接在参数中进行实例化合约
function setX(Callee _callee, uint _x) public {
uint x = _callee.setX(_x);
}
// 传递地址,在内部实例化callee合约
function setXFromAddress(address _addr, uint _x) public {
Callee callee = Callee(_addr);
callee.setX(_x);
}
// 调用方法,并转ether
function setXandSendEther(Callee _callee, uint _x) public payable {
(uint x, uint value) = _callee.setXandSendEther{value: msg.value}(_x);
}
}
new合约
创建合约时,在世界状态中,增加一个地址与账户的信息。
在合约内部可以使用关键字new创建新的合约;
在0.8.0版本之后,new增加了salt选项,从而支持了create2的特性(通过salt可以计算出创建合约的地址)。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Car {
address public owner;
string public model;
address public carAddr;
constructor(address _owner, string memory _model) payable {
owner = _owner;
model = _model;
carAddr = address(this);
}
}
contract CarFactory {
Car[] public cars;
function create(address _owner, string memory _model) public {
Car car = new Car(_owner, _model);
cars.push(car);
}
function createAndSendEther(address _owner, string memory _model) public payable {
Car car = (new Car){value: msg.value}(_owner, _model);
cars.push(car);
}
function create2(
address _owner,
string memory _model,
bytes32 _salt
) public {
Car car = (new Car){salt: _salt}(_owner, _model);
cars.push(car);
}
function create2AndSendEther(
address _owner,
string memory _model,
bytes32 _salt
) public payable {
Car car = (new Car){value: msg.value, salt: _salt}(_owner, _model);
cars.push(car);
}
function getCar(uint _index)
public
view
returns (
address owner,
string memory model,
address carAddr,
uint balance
)
{
Car car = cars[_index];
return (car.owner(), car.model(), car.carAddr(), address(car).balance);
}
}
delete
- delete操作符可以用于任何变量(map除外),将其设置成默认值;
- 如果对动态数组使用delete,则删除所有元素,其长度变为0: uint[ ] array0 ; arry0 = new uint;
- 如果对静态数组使用delete,则重置所有索引的值,数组长度不变: uint[10] array1 = [1,2,3,4,5,6];
- 如果对map类型使用delete,什么都不会发生;
- 但如果对map类型中的一个键使用delete,则会删除与该键相关的值。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Delete {
//01. string
string public str1 = "hello";
function deleteStr() public {
delete str1;
}
function setStr(string memory input) public {
str1 = input;
}
//02. array 对于固定长度的数组,会删除每个元素的值,但是数组长度不变
uint256[10] public arry1 = [1,2,3,4,5];
function deleteFiexedArry() public {
delete arry1;
}
//03. array new
uint256[] arry2 ;
function setArray2() public {
arry2 = new uint256[](10);
for (uint256 i = 0; i< arry2.length; i++) {
arry2[i] = i;
}
}
function getArray2() public view returns(uint256[] memory) {
return arry2;
}
function deleteArray2() public {
delete arry2;
}
//04. mapping
mapping(uint256 => string) public m1;
function setMap() public {
m1[0] = "hello";
m1[1] = "world";
}
//Mapping不允许直接使用delete,但是可以对mapping的元素进行指定删除
// function deleteM1() public {
// delete m1;
// }
function deleteMapping(uint256 i) public {
delete m1[i];
}
}
try/catch
try/catch仅可以捕捉在调用external函数或创建合约中抛出的异常信息。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// External contract used for try / catch examples
contract Foo {
address public owner;
constructor(address _owner) {
require(_owner != address(0), "invalid address");
assert(_owner != 0x0000000000000000000000000000000000000001);
owner = _owner;
}
function myFunc(uint x) public pure returns (string memory) {
require(x != 0, "require failed");
return "my func was called";
}
}
contract Bar {
event Log(string message);
event LogBytes(bytes data);
Foo public foo;
constructor() {
// This Foo contract is used for example of try catch with external call
foo = new Foo(msg.sender);
}
// Example of try / catch with external call
// tryCatchExternalCall(0) => Log("external call failed")
// tryCatchExternalCall(1) => Log("my func was called")
function tryCatchExternalCall(uint _i) public {
try foo.myFunc(_i) returns (string memory result) {
emit Log(result);
} catch {
emit Log("external call failed");
}
}
// Example of try / catch with contract creation
// tryCatchNewContract(0x0000000000000000000000000000000000000000) => Log("invalid address")
// tryCatchNewContract(0x0000000000000000000000000000000000000001) => LogBytes("")
// tryCatchNewContract(0x0000000000000000000000000000000000000002) => Log("Foo created")
function tryCatchNewContract(address _owner) public {
try new Foo(_owner) returns (Foo foo) {
// you can use variable foo here
emit Log("Foo created");
} catch Error(string memory reason) {
// catch failing revert() and require()
emit Log(reason);
} catch (bytes memory reason) {
// catch failing assert()
emit LogBytes(reason);
}
}
}
没有错误信息的时候,直接使用catch比较稳妥,如果使用reason,会捕捉require发出的信息。
import
我们可以使用import将本地的.sol文件或外部(github或openzeppelin等).sol导入进来
├── Import.sol
└── Foo.sol
Fool.sol
常量、函数、枚举、结构体、Error可以定义在合约之外;事件、变量不允许定义在合约之外。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
struct Point {
uint x;
uint y;
}
// 事件不允许定义在合约之外
// event Greeting(string);
error Unauthorized(address caller);
string constant greeting = "hell world";
function add(uint x, uint y) pure returns (uint) {
return x + y;
}
contract Foo {
string public name = "Foo";
}
Import.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// import Foo.sol from current directory
import "./Foo.sol";
// import {symbol1 as alias, symbol2} from "filename";
import {Unauthorized, add as func, Point} from "./Foo.sol";
contract Import {
// Initialize Foo.sol
Foo public foo = new Foo();
// Test Foo.sol by getting it's name.
function getFooName() public view returns (string memory) {
return foo.name();
}
function myAdd() public pure returns(uint) {
return func(1,2);
}
function greetingCall() public pure returns(string memory) {
return greeting;
}
}
导入外部文件:
// https://github.com/owner/repo/blob/branch/path/to/Contract.sol
import "https://github.com/owner/repo/blob/branch/path/to/Contract.sol";
// Example import ECDSA.sol from openzeppelin-contract repo, release-v4.5 branch
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol";
Try on Remix
节约gas
- 使用calldata替换memory
- 将状态变量加载到memory中
- 使用++i替换i++
- 对变量进行缓存
- 短路效应
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// gas golf
contract GasGolf {
// start - 50908 gas
// use calldata - 49163 gas
// load state variables to memory - 48952 gas
// short circuit - 48634 gas
// loop increments - 48244 gas
// cache array length - 48209 gas
// load array elements to memory - 48047 gas
uint public total;
// start - not gas optimized
// function sumIfEvenAndLessThan99(uint[] memory nums) external {
// for (uint i = 0; i < nums.length; i += 1) {
// bool isEven = nums[i] % 2 == 0;
// bool isLessThan99 = nums[i] < 99;
// if (isEven && isLessThan99) {
// total += nums[i];
// }
// }
// }
// gas optimized
// [1, 2, 3, 4, 5, 100]
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;
}
}
十种节约方法
一共两种类型的Gas需要消耗,他们是:
- 调用合约
- 部署合约
有时候,减少了一种类型的gas会导致另一种类型gas的增加,我们需要进行权衡(Tradeoff)利弊,主要优化方向:
- Minimize on-chain data (events, IPFS, stateless contracts, merkle proofs) -> 优化链上存储
- Minimize on-chain operations (strings, return storage value, looping, local storage, batching) -> 优化链上操作
- Memory Locations (calldata, stack, memory, storage) -> 数据位置的选择
- Variables ordering -> 变量的定义顺序很重要
- Preferred data types -> 数据类型的选择
- Libraries (embedded, deploy) -> 尽量使用库来减少部署gas
- Minimal Proxy -> 使用clone方式创建新合约
- Constructor -> 优化构造函数(尽量使用constant)
- Contract size (messages, modifiers, functions) -> 优化合约size
- Solidity compiler optimizer -> 开启优化中
1. Minimize on-chain data
- Event:如果链上合约不需要调用的数据,可以使用event,由链下监听,提供只读操作;
- IPFS:大数据可以上传到ipfs,然后将对应的id存储在链上;
- 无状态合约:如果只是为了存储key-value,那么在合约中不需要状态变量存储,而是仅仅通过参数记录,让链下程序去解析交易,读取参数,从而读取到key-value数据;
- 默克尔根(Merkle Proofs):快速验证数据,合约不用存储太多内容。
2. Minimize on-chain operations
- string:string内在也是bytes,尽量使用bytes替代,可以减少EVM计算,减少gas消耗;
- 返回storage值:直接返回storage如果有必要的话,具体内部数据,让链下程序解析;
- Local Storage:使用local storage变量,可节约开销,不要使用memory进行copy一遍操作;
- Batching(批量操作):如果有批量操作需要,可以提供相应接口,避免用户发起相同交易。
3. Memory locations
四种存储位置gas消耗(由低到高):calldata -> stack -> memory -> storage.
- Calldata:一般用在参数中,修饰引用数据类型(array、string),限定external function,尽量使用,便宜;
- Stack:函数体中值类型的数据,自动修饰为stack类型;
- Memory:对于存储引用类型的数据时,完全拷贝(你没有看错,反而更便宜)比storage便宜;
- Storage:最贵,非必要,不使用。
4. Variables ordering
- Storage slots(槽)大小是32字节,但并不是所有的类型都能填满(bool,int8等);
- 调整顺序,可以优化storage使用空间:
- uint128、uint128、uint256,一共使用两个槽位(good)✅
- uint128、uint256、uint128,一共使用三个槽位(bad)❌
5. Preferred data types
- 如果定义变量的类型原本可以填满整个槽位,那么就填满ta,而不要使用更短的数据类型。
- 例如:如果定义数据类型:datatype:uint8,但是opcode原则上是处理:uint256的,那么会对空余部分填充:0,这反而会增加evm对gas的开销,所以更好的方法是:直接定义datatype为:uint256。
6. Libraries
库有两种表现形式:
- Embedded Libraries:当lib中的方法都是internal的时候,会自动内联到合约中,此时对节约gas不起作用;
- Deployed Libraries:当lib中有public或external方法时,此时会单独部署lib合约,我们可以使用一个lib地址关联到不同合约来达到节约gas的目的。
7. Minimal Proxies (ERC 1167)
- 这是一个标准,用于clone合约
- openzeppelin合约中clone就源于此
8. Constructor
- 构造函数中可以传递immutable数据,如果可能,尽量使用constant,这样开销更小。
9. Contract Size
- 合约最大支持24K
- 减少Logs/ Message:require后面的des,event的使用,都影响合约size
- 使用opcode:这个看情况而定,opcode可能减少部署开销,却引来调用开销的增加。
- 修饰器Modifier:modifer中wrapped一个函数,在函数中实现具体逻辑 // TODO
10. Solidity compiler optimizer
- 开启编译器optimize,这个是有双面性的,一定会使得合约size变小;
- 但是可能会使部分函数的逻辑变复杂(code bigger),增加函数的执行开销。
参考链接:https://medium.com/coinmonks/smart-contracts-gas-optimization-techniques-2bd07add0e86
type
https://docs.soliditylang.org/en/v0.6.5/units-and-global-variables.html#meta-type
type(x) 可以返回x类型的对象信息,例如:
- type(x).name: 合约的名字;
- type(x).creattionCode: 合约部署时的bytecode;
- type(x).runtimeCode: 合约运行时的bytecode,一般是构造函数数据,但是当constructor中有汇编时会有不同(没有仔细了解)。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/**
* @title Storage
* @dev Store & retrieve value in a variable
*/
contract Storage {
string public str;
constructor(string memory _str) {
str = _str;
}
uint256 number;
/**
* @dev Store value in variable
* @param num value to store
*/
function store(uint256 num) public {
number = num;
}
/**
* @dev Return value
* @return value of 'number'
*/
function retrieve() public view returns (uint256){
return number;
}
function getInfo() public pure returns(string memory name) {
name = type(Storage).name;
// creationCode 和runtimeCode不能在这个合约自己内部使用,防止会出现循环调用问题
// creationCode = type(Storage).creationCode;
// runtimeCode = new type(Storage).runtimeCode;
}
}
contract TestStorage {
Storage s;
constructor(Storage _address) {
s = _address;
}
function getInfo() public view returns(bytes memory creationCode, bytes memory runtimeCode) {
creationCode = type(Storage).creationCode;
runtimeCode = type(Storage).runtimeCode;
}
}
汇编
- 汇编在写库的时候很实用,提供更加细颗粒度的编写
- 汇编语言为Yul语法
- 使用assembly { … }包裹
节约gas
测试:input: [1,2,3]
- 23374 gas -> sumSolidity(最低效)
- 23082 gas -> sumAsm(次之)
- 22895 gas -> sumPureAsm(最高效)
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
library VectorSum {
// This function is less efficient because the optimizer currently fails to
// remove the bounds checks in array access.
function sumSolidity(uint[] memory data) public pure returns (uint sum) {
for (uint i = 0; i < data.length; ++i)
sum += data[i];
}
// We know that we only access the array in bounds, so we can avoid the check.
// 0x20 needs to be added to an array because the first slot contains the
// array length.
function sumAsm(uint[] memory data) public pure returns (uint sum) {
for (uint i = 0; i < data.length; ++i) {
assembly {
sum := add(sum, mload(add(add(data, 0x20), mul(i, 0x20))))
}
}
}
// Same as above, but accomplish the entire code within inline assembly.
function sumPureAsm(uint[] memory data) public pure returns (uint sum) {
assembly {
// Load the length (first 32 bytes)
let len := mload(data)
// Skip over the length field.
//
// Keep temporary variable so it can be incremented in place.
//
// NOTE: incrementing data would result in an unusable
// data variable after this assembly block
let dataElementLocation := add(data, 0x20)
// Iterate until the bound is not met.
for
{ let end := add(dataElementLocation, mul(len, 0x20)) }
lt(dataElementLocation, end)
{ dataElementLocation := add(dataElementLocation, 0x20) }
{
sum := add(sum, mload(dataElementLocation))
}
}
}
}
其他文章
Event Log
概述
https://ethereum.stackexchange.com/questions/3418/how-does-ethereum-make-use-of-bloom-filters/3426
- Log也在区块链账本中,但是它不是链上数据;
- Log是由交易执行产生的数据,它是不需要共识的,可以通过重新执行交易生成;
- Log是经由链上校验的,无法造假,因为一笔交易的ReceiptHash是存在链上的(Header中)
- 日志在账本中,和合约存储在不同的结构中;
- log不能被合约读取,可以被链下读取;
- 一部分log(使用indexed修饰的字段)存储在bloom filters中,可以由轻节点随意访问;
- 日志是账本的一部分:https://docs.soliditylang.org/en/develop/contracts.html?highlight=events#events
Event
- indexed:方便索引,加了inexed是topics
- non-indexed:没有解码,需要使用abi解码后才知道内容,不加indexd是data
描述
发送一笔交易之后,我们可以得到tx交易的返回值,在返回值中会包含TransactionRecept字段,进一步会发现Event,指定id可以获取这个事件的详情,进一步会获得每个字段的具体数值。
console.log(transactionReceipt.events[0].args.oldNumber.toString())
Bloom
以太坊的Header中有布隆过滤器,这里面存放的是什么?(是所有事件生成的信息),当有索引过来的时候,我们会去重新生成事件。
- 只有indexed的field才会添加到bloom filter中
- 有请求的时候会快速在bloom filter中寻找,如果发现存在,则重新执行交易,并且返回log结果

