Solidity语法(下)
小白入门:https://github.com/dukedaily/solidity-expert ,欢迎star转发,文末加V入群。
职场进阶: https://dukeweb3.com
讲师介绍
资深web3开发者,bybit交易所defi团队Tech Lead,MoleDAO技术顾问,国内第一批区块链布道者,专注海外defi,dex,AA钱包等业务方向。
- 公众号:阿杜在新加坡
- github:以太坊教程
- B站:杜旭duke
- Youtube:duke du
- Twitter:dukedu2022
interface
可以使用Interface完成多个合约之间进行交互,interface有如下特性:
- 接口中定义的function不能存在具体实现;
- 接口可以继承;
- 所有的function必须定义为external;public,internal,private
- 接口中不能存在constructor函数;
- 接口中不能定义状态变量;
- abstract和interface的区别
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Counter {
uint public count;
function increment() external {
count += 1;
}
}
interface IBase {
function count() external view returns (uint);
}
interface ICounter is IBase {
// uint num;
function increment() external;
}
contract MyContract {
function incrementCounter(address _counter) external {
ICounter(_counter).increment();
}
function getCount(address _counter) external view returns (uint) {
return ICounter(_counter).count();
}
}
uniswap demo:
// Uniswap example
interface UniswapV2Factory {
function getPair(address tokenA, address tokenB)
external
view
returns (address pair);
}
interface UniswapV2Pair {
function getReserves()
external
view
returns (
uint112 reserve0,
uint112 reserve1,
uint32 blockTimestampLast
);
}
contract UniswapExample {
address private factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f;
address private dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
address private weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
function getTokenReserves() external view returns (uint, uint) {
address pair = UniswapV2Factory(factory).getPair(dai, weth);
(uint reserve0, uint reserve1, ) = UniswapV2Pair(pair).getReserves();
return (reserve0, reserve1);
}
}
library
库与合约类似,限制:不能在库中定义状态变量,不能向库地址中转入ether,库有两种存在形式:
- 内嵌(embedded):当库中所有的方法都是internal时,此时会将库代码内嵌在调用合约中,不会单独部署库合约;
- ==链接(linked)==:当库中含有external或public方法时,此时会单独将库合约部署,并在调用合约部署时链接link到库合约。
- 可以复用的代码可以编写到库中,不同的调用者可以linked到相同的库,因此会更加节约gas;
- 对于linked库合约,调用合约使用delegatecall进行调用,所以上下文为调用合约;
- 部署工具(如remix)会帮我们自动部署&链接合约库。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// 1. 只有internal方法,会内嵌到调用合约中
library SafeMath {
function add(uint x, uint y) internal pure returns (uint) {
uint z = x + y;
require(z >= x, "uint overflow");
return z;
}
}
library Math {
function sqrt(uint y) internal pure returns (uint z) {
if (y > 3) {
z = y;
uint x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
// else z = 0 (default value)
}
}
contract TestSafeMath {
// 对uint类型增加SafeMath的方法,
// 1. 后续定义的uint变量就会自动绑定SafeMath提供的方法: uint x;
// 2. 这个变量会作为第一个参数传递给函数: x.add(y);
using SafeMath for uint;
uint public MAX_UINT = 2**256 - 1;
// 用法1:x.方法(y)
function testAdd(uint x, uint y) public pure returns (uint) {
//return x.add(y);
return SafeMath.add(x,y);
}
// 用法2:库.方法(x)
function testSquareRoot(uint x) public pure returns (uint) {
return Math.sqrt(x);
}
}
// 2. 存在public方法时,会单独部署库合约,并且第一个参数是状态变量类型
library Array {
// 修改调用者状态变量的方式,第一个参数是状态变量本身
function remove(uint[] storage arr, uint index) public {
// Move the last element into the place to delete
require(arr.length > 0, "Can't remove from empty array");
arr[index] = arr[arr.length - 1];
arr.pop();
}
}
contract TestArray {
using Array for uint[];
uint[] public arr;
function testArrayRemove() public {
for (uint i = 0; i < 3; i++) {
arr.push(i);
}
arr.remove(1);
assert(arr.length == 2);
assert(arr[0] == 0);
assert(arr[1] == 2);
}
}
encode
- abi.encode:可以将data编码成bytes,生成的bytes总是32字节的倍数,不足32为会自动填充(用于给合约调用);
- abi.decode:可以将bytes解码成data(可以只解析部分字段)
- abi.encodePacked:与abi.encode类似,但是生成的bytes是压缩过的(有些类型不会自动填充,无法传递给合约调用)。
- 手册:https://docs.soliditylang.org/en/v0.8.13/abi-spec.html?highlight=abi.encodePacked#non-standard-packed-mode
// 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
*/
}
// 可以用encodePacked来拼接字符串
// output string: ipfs://bafybeidmrsvehl4ehipm5qqvgegi33r6/100.json
function encodePackedTest() public pure returns (string memory) {
string memory uri = "ipfs://bafybeidmrsvehl4ehipm5qqvgegi33r6/";
return string(abi.encodePacked(uri, "100", ".json"));
}
}
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;
}
}
Send Ether
如何发送ether?
有三种方式可以向合约地址转ether:
send(21000 gas,return bool)- transfer(21000 gas, throw error)
- call(传递交易剩余的gas或设置gas,不限定21000gas,return bool)(推荐使用)
总结:transfer() 和 send() 函数使用 2300 gas 以防止重入攻击,但公链升级后可能导致 gas 不足。所以推荐使用 call() 函数,但需做好重入攻击防护。
如何接收ether?
想接收ether的合约至少包含以下方法中的一个:
- receive() external payable:msg.data为空时调用(为接收ether而生,仅solidity 0.6版本之后)
- fallback() external payable:msg.data非空时调用(为执行default逻辑而生,顺便支持接收ether)
// 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");
}
}
解析:
调用sendViaTransfer或sendViaSend的时候,假设构造这笔交易时,你传入的gas时:1000000 gas
此时,在使用transfer和send转账的时候,只会传递2300个gas,如果接收者是个合约,这个合约必须有fallback,此时这个fallback里面不能有逻辑,否则会超过2300gas,导致转账失败。
sendViaCall的时候,假设构造这笔交易时,你传入的gas时:1000000 gas 此时在调用call的时候,也可以完成转账,但是会把1000000传递给fallback,即在fallback中你可以实现自己复杂的逻辑。
参考链接:https://docs.soliditylang.org/en/latest/security-considerations.html#sending-and-receiving-ether
call
call是一种底层调用合约的方式,可以在合约内调用其他合约
当调用fallback方式给合约转ether的时候,建议使用call,而不是使用transfer或send方法
对于存在的方法,不建议使用call方式调用
调用不存在的方法(又不存在fallback)时,交易会调用成功,但是第一个参数为:false,所以使用call调用后一定要检查success状态
// 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);
}
}
staticcall
- https://eips.ethereum.org/EIPS/eip-214
- Since byzantium staticcall can be used as well. This is basically the same as call, but will revert if the called function modifies the state in any way.
- 与CALL相同,但是不允许修改任何状态变量,是为了安全🔐考虑而新增的OPCODE
- 在Transparent模式的代理合约逻辑中,就使用了staticcall,从而让proxyAmin能够免费的调用父合约的admin函数,从而从slot中返回代理合约的管理员。这部分会在合约升级章节介绍。
function getProxyAdmin(TransparentUpgradeableProxy proxy) public view virtual returns (address) {
// We need to manually run the static call since the getter cannot be flagged as view
// bytes4(keccak256("admin()")) == 0xf851a440
(bool success, bytes memory returndata) = address(proxy).staticcall(hex"f851a440");
require(success);
return abi.decode(returndata, (address));
}
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 Implementation {
// 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;
}
}
contract ImplementationV2 {
// 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*2;
sender = msg.sender;
value = msg.value;
}
}
// 注意:执行后,Proxy中的sender值为EOA的地址,而不是A合约的地址 (调用链EOA-> Proxy::setVars -> Implementation::setVars)
contract Proxy {
uint public num;
address public sender;
uint public value;
function setVars(address _impl, uint _num) public payable {
// Proxy's storage is set, Implementation is not modified.
(bool success, bytes memory data) = _impl.delegatecall(
abi.encodeWithSignature("setVars(uint256)", _num)
);
}
}
create&create2
创建合约时,在世界状态中,增加一个地址与账户的信息。
在EVM层面,一共有两个操作码(OPCODE)可以用来创建合约:
create:
原理:新生成地址 = hash(创建者地址, nonce)
特点:不可预测,因为nonce是变化的
create2:
原理:新生成地址 = hash("0xFF",创建者地址, salt, bytecodeHash)
- 特点:可以预测,因为没有变量
在编码时,我们可以直接使用汇编来创建新合约,也可以使用solidity中的new关键字来创建新合约:
- 使用汇编方式:
assembly {
create(参数...)
}
assembly {
create2(参数...)
}
- 使用new方式创建:
// 内部调用create
new ContractName(参数...)
// 内部调用create2
// 在0.8.0版本之后,new增加了salt选项,从而支持了create2的特性(通过salt可以计算出创建合约的地址)。
new ContractName{salt: _salt}(参数...)
demo验证:
// 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);
}
}
合约间调用
普通的交易,相当于在世界状态中修改原有的账户数据,更新到新状态。
一共有三种方式调用合约:
- 使用合约实例调用合约(常规):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);
}
}