Solidity全局变量完全测试
我们知道,在Solidity中有很多全局变量,例如我们最常用的msg.sender, block.timestamp
等。但是有一些我们平常基本接触不到,例如:type(C).name
等。本着凡事最怕认真两字的原则,虽然繁琐,但我们将所有的全局变量全部测试一遍,学习怎么调用和应用在哪些场景,进一步加深理解与记忆。
本文基于Solidity 0.8.9版本与hardhat
工具进行,在最新的0.8.13
版本增加了两个全局变量abi.encodeCall
与string.concat
,因当前版本的hardhat
暂不支持 Solidity 0.8.13
,故没有进行这两项测试。 另外,有少数项目也不方便测试,期待有人能改进完善测试方法。
我们的测试合约如下:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.9;
interface IERC721 {
function balanceOf(address _owner) external view returns (uint256);
function ownerOf(uint256 _tokenId) external view returns (address);
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata data) external payable;
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
function approve(address _approved, uint256 _tokenId) external payable;
function setApprovalForAll(address _operator, bool _approved) external;
function getApproved(uint256 _tokenId) external view returns (address);
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}
contract NoReceiveEth {
function originTest(address instance) public view returns(address) {
return GlobalVariables(instance).originTest();
}
}
contract Base {
function getUint(uint a) public pure virtual returns(uint) {
return a;
}
}
/// 全局变量测试
contract GlobalVariables is Base{
event SendEther(address indexed receipt,uint amount, bool result);
event ReturnBaseFee(uint fee);
event GasLeft(uint gas);
event SelfDestruct(address indexed recipient);
// 解码数据
// 这里只适合类型提前知道的情况,因为Solidity类型无法作为参数,所以这里采用了固定类型进行测试
function decodeTest(bytes memory data) public pure returns(uint a, uint[2] memory b, bytes memory c) {
(a,b,c) = abi.decode(data,(uint, uint[2], bytes));
}
//编码数据,这里可以和解码数据联合测试
function encodeTest(uint a, uint[2] memory b, bytes memory c) public pure returns(bytes memory) {
return abi.encode(a,b,c);
}
//压缩编码,注意它会引起混淆,一般用于哈希计算时,也就是不需要解码操作
function encodePackedTest(uint a, uint[2] memory b, bytes memory c) public pure returns(bytes memory) {
return abi.encodePacked(a,b,c);
}
//根据函数选择器编码,其实就是函数选择器加上前面的编码,通常用于底层函数调用,例如SafeTransfer。
function encodeWithSelectorTest(bytes4 selector,uint a, uint[2] memory b, bytes memory c) public pure returns(bytes memory) {
return abi.encodeWithSelector(selector, a,b,c);
}
// 0.8.13版本才有,hardhat暂不支持这么高版本,故skip
// function(uint, uint[2] memory, bytes memory ) functionPointer;
// //直接根据函数编码,其结果和encodeWithSelector相同,但会多一个参数类型检查
// function encodeCallTest(
// uint a,
// uint[2] memory b,
// bytes memory c
// ) public pure returns(bytes memory) {
// return abi.encodeCall(functionPointer,a,b,c);
// }
//根据函数名称编码,这里其实给出了函数选择器的计算方法: bytes4(keccak256(bytes(signature))
// 结果和 abi.encodeWithSelector(bytes4(keccak256(bytes(signature)), ...) 相同
// 注意signature的写法:例如: "testFunc(uint256,uint256[2],bytes)"
function encodeWithSignatureTest(
string memory signature,
uint a,
uint[2] memory b,
bytes memory c
) public pure returns(bytes memory) {
return abi.encodeWithSignature(signature,a,b,c);
}
// 连接bytes 、bytes1 - bytes32,注意,它并不会padding
function bytesConcatTest(bytes memory a, bytes memory b, bytes memory c) public pure returns(bytes memory) {
return bytes.concat(a,b,c);
}
// //字符串连接,很直观 0.8.13版本才有,hardhat暂不支持这么高版本,故skip
// function stringConcatTest(string memory a, string memory b, string memory c) public pure returns(string memory) {
// return string.concat(a,b,c);
// }
// EIP-1559 基础fee
function basefeeTest() public returns(uint) {
emit ReturnBaseFee(block.basefee);
return block.basefee;
}
// 以下是获取各种区块信息
function chainidTest() public view returns(uint) {
return block.chainid;
}
function coinbaseTest() public view returns(address payable) {
return block.coinbase;
}
function difficultyTest() public view returns(uint) {
return block.difficulty;
}
function gaslimitTest() public view returns(uint) {
return block.gaslimit;
}
function numberTest() public view returns(uint) {
return block.number;
}
function timestampTest() public view returns(uint) {
return block.timestamp;
}
// 不是很好测试
function gasleftTest() public returns(uint) {
uint gas = gasleft();
emit GasLeft(gas);
return gas;
}
function msgDataTest(uint , uint[2] memory , bytes memory ) public pure returns(bytes memory) {
return msg.data;
}
function msgSenderTest() public view returns(address) {
return msg.sender;
}
function msgSigTest(uint , uint[2] memory , bytes memory ) public pure returns(bytes4) {
return msg.sig;
}
function msgValueTest() payable public returns(uint) {
emit SendEther(address(this),msg.value,true);
return msg.value;
}
function gasPriceTest() public view returns(uint) {
return tx.gasprice;
}
function originTest() public view returns(address) {
require(msg.sender == tx.origin, "Sender is a contract");
return tx.origin;
}
// 抛出异常
function assertTest(uint a) public pure returns(bool) {
assert(a > 5);
return true;
}
function requireTest(uint a) public pure returns(bool) {
require(a > 5);
return true;
}
function requireTest(uint a , string memory reason) public pure returns(bool) {
require(a > 5 , reason);
return true;
}
function revertTest(bool isRevert) public pure returns(bool) {
if(isRevert) {
revert();
}
return true;
}
function revertTest(bool isRevert, string memory reason) public pure returns(bool) {
if(isRevert) {
revert(reason);
}
return true;
}
// 加密算法
function blockhashTest(uint blockNumber) public view returns(bytes32) {
return blockhash(blockNumber);
}
function keccak256Test(bytes memory data) public pure returns(bytes32) {
return keccak256(data);
}
function sha256Test(bytes memory data) public pure returns(bytes32) {
return sha256(data);
}
function ripemd160Test(bytes memory data) public pure returns(bytes20) {
return ripemd160(data);
}
// 注意这里第一个参数为messageHash,不是message
function ecrecoverTest(bytes32 hash, uint8 v, bytes32 r, bytes32 s) public pure returns(address) {
return ecrecover(hash,v,r,s);
}
// 注意这里重点是可溢出
function addmodTest(uint x, uint y, uint k) public pure returns(uint) {
return addmod(x,y,k);
}
function mulmodTest(uint x, uint y, uint k) public pure returns(uint) {
return mulmod(x, y, k);
}
// this 代表本合约
function thisTest() public view returns(address) {
return address(this);
}
// 注意 this.call代表一个外部调用而不是内部跳转
function thisCallTest() public view returns(address) {
return this.msgSenderTest();
}
// 调用父合约函数
function superTest(uint a) public pure returns(uint) {
if(a > 10) {
return super.getUint(a);
} else {
return a - 2;
}
}
// 自杀,注意这里转移ETH会无视合约是否接收ETH
function selfdestructTest(address payable recipient) public {
emit SelfDestruct(recipient);
selfdestruct(recipient);
// 注意,这后面的语句可能无法执行
}
function balanceTest() public view returns(uint) {
return address(this).balance;
}
//这里的code不知道该怎么测试
function codeTest() public view returns(bytes memory) {
return address(this).code;
}
function codehashTest() public view returns(bytes32) {
return address(this).codehash;
}
// 注意发送失败返回falses
function sendTest(address payable recipient, uint amount) public returns(bool result) {
result = recipient.send(amount);
emit SendEther(recipient,amount,result);
}
// 发送失败会重置
function transferTest(address payable recipient, uint amount) public {
recipient.transfer(amount);
emit SendEther(recipient,amount,true);
}
// 下面的信息编译后可获取
function contractNameTest() public pure returns(string memory) {
return type(GlobalVariables).name;
}
function creationCodeTest() public pure returns(bytes memory) {
return type(Base).creationCode;
}
function runtimeCodeTest() public pure returns(bytes memory) {
return type(Base).runtimeCode;
}
/**
* 0x70a08231 ^ 0x6352211e ^ 0x095ea7b3 ^ 0x081812fc ^
0xa22cb465 ^ 0xe985e9c ^ 0x23b872dd ^ 0x42842e0e ^ 0xb88d4fde = 0x80ac58cd
*/
function interfaceIdTest() public pure returns(bytes4) {
return type(IERC721).interfaceId; // 0x80ac58cd
}
// 最小值,最大值,常用来替代uint(-1)
function minTest() public pure returns(int256) {
return type(int256).min;
}
function maxTest() public pure returns(uint256) {
return type(uint).max;
}
}
合约中部分知识点有简单注释。现在合约有了,我们要开始调用了,当然单元测试是最适合这项工作的。
单元测试文件因为测试的内容较多,我们新建了两个单元测试文件 GlobalVariables-01.js
和GlobalVariables-02.js
。
// GlobalVariables-01.js
const { expect, assert } = require("chai");
const { ethers } = require("hardhat");
describe("GlobalVariables Test 01", function () {
let instance;
let owner,user,addrs;
let test_contract;
const abi = [
"function testFunc(uint a, uint[2] memory b, bytes memory c) public view returns(uint256)"
]
const interface = new ethers.utils.Interface(abi);
const args = [
10245,
[2345,1098],
"0x12345678"
]
const abiCode = new ethers.utils.AbiCoder();
before( async function() {
const GlobalVariables = await ethers.getContractFactory("GlobalVariables");
instance = await GlobalVariables.deploy();
const NoReceiveEth = await ethers.getContractFactory("NoReceiveEth");
test_contract = await NoReceiveEth.deploy();
[owner, user, ...addrs] = await ethers.getSigners();
});
describe("Encode and Decode test", function() {
it("Encode test", async () => {
let data = abiCode.encode(
["uint","uint[2]","bytes"],
args
)
expect(await instance.encodeTest(...args)).to.be.equal(data)
})
it("Decode test", async () => {
let data = abiCode.encode(
["uint","uint[2]","bytes"],
args
)
const [a,b,c] = await instance.decodeTest(data);
assert.equal(a,args[0])
assert.equal(b[0],args[1][0])
assert.equal(b[1],args[1][1])
assert.equal(c,args[2])
});
it("EncodePacked test", async () => {
let data = abiCode.encode(
["uint","uint[2]"],
[args[0],args[1]]
)
data += args[2].substring(2,args[2].length);
expect (await instance.encodePackedTest(...args)).to.be.equal(data);
})
it("EncodeWithSelector test", async () => {
const selector = interface.getSighash("testFunc");
let data = interface.encodeFunctionData("testFunc",args);
expect (await instance.encodeWithSelectorTest(selector,...args)).to.be.equal(data);
let encode = await instance.encodeTest(...args);
encode = encode.substring(2,encode.length); //去掉0x显示
assert.equal(selector + encode, data);
});
it("EncodeWithSignature test", async () => {
const selector = interface.getSighash("testFunc");
let encode = await instance.encodeTest(...args);
encode = encode.substring(2,encode.length); //去掉0x显示
expect(await instance.encodeWithSignatureTest(
"testFunc(uint256,uint256[2],bytes)",
...args
)).to.be.equal(selector + encode);
})
});
describe("bytesConcat test", () => {
it("Hello world test", async () => {
const hello = ethers.utils.toUtf8Bytes("hello")
const blank = ethers.utils.toUtf8Bytes(" ")
const world = ethers.utils.toUtf8Bytes("world")
let hello_world = ethers.utils.hexlify(
ethers.utils.toUtf8Bytes("hello world")
);
expect (await instance.bytesConcatTest(hello,blank,world)).to.be.equal(hello_world);
})
})
describe("Block Info test", () => {
/**
* 这里有瑕疵
* 如果直接使用npx hardhat test ,这里的baseFee是会逐渐减小的
* 但是如果使用npx hardhat coverage,这里的baseFee会是0,应该不正确。
*/
it("Basefee test", async () => {
let tx = await instance.basefeeTest({
gasLimit:300000
});
let receipt = await tx.wait();
let fee = receipt.events[0].args.fee;
await expect(instance.basefeeTest({
gasLimit:300000
})).to.emit(instance,"ReturnBaseFee").withArgs(fee);
});
it("chainid test", async () => {
let chainId = ethers.provider.network.chainId;
expect(await instance.chainidTest()).to.be.equal(chainId);
});
it("coinbase test", async () => {
let block = await ethers.provider.getBlock();
expect(await instance.coinbaseTest()).to.be.equal(block.miner);
})
it("Difficulty test", async () => {
let block = await ethers.provider.getBlock();
expect(await instance.difficultyTest()).to.be.equal(block.difficulty);
})
it("Gaslimit test", async () => {
let block = await ethers.provider.getBlock();
expect(await instance.gaslimitTest()).to.be.equal(block.gasLimit);
})
it("Block number test", async () => {
let block = await ethers.provider.getBlock();
expect(await instance.numberTest()).to.be.equal(block.number);
})
it("Timestamp test", async () => {
let block = await ethers.provider.getBlock();
expect(await instance.timestampTest()).to.be.equal(block.timestamp);
})
it("Gasleft test", async () => {
let tx = await instance.gasleftTest({
gasLimit:300000
});
let receipt = await tx.wait();
let gas = receipt.events[0].args.gas;
await expect(instance.gasleftTest({
gasLimit:300000
})).to.emit(instance,"GasLeft").withArgs(gas)
});
it("BlockHashTest", async () => {
let blockNumber = await ethers.provider.getBlockNumber();
let curBlock = await ethers.provider.getBlock(blockNumber)
let prevBlock = await ethers.provider.getBlock(blockNumber -1)
assert.equal(curBlock.parentHash,prevBlock.hash) // block chain
expect(await instance.blockhashTest(blockNumber -1)).to.be.equal(prevBlock.hash)
});
});
describe("Msg test", () => {
it("Msg data test", async () => {
let data = instance.interface.encodeFunctionData("msgDataTest",args);
expect(await instance.msgDataTest(...args)).to.be.equal(data)
});
it("Msg sender test", async () => {
expect(await instance.msgSenderTest()).to.be.equal(owner.address)
expect(await instance.connect(user).msgSenderTest()).to.be.equal(user.address)
});
it("Msg Sig test", async () => {
let selector = instance.interface.getSighash("msgSigTest");
expect(await instance.msgSigTest(...args)).to.be.equal(selector)
});
it("Msg value test", async () => {
await expect(instance.msgValueTest({
value:ethers.utils.parseEther("1.0")
})).to.emit(instance,"SendEther").withArgs(instance.address,ethers.utils.parseEther("1.0"),true)
});
it("Gas price test", async () => {
expect(await instance.gasPriceTest({
gasPrice:500
})).to.be.equal(500)
});
it("OriginTest successful", async () => {
expect(await instance.originTest()).to.be.equal(owner.address);
});
it("OriginTest failed", async () => {
await expect(test_contract.originTest(instance.address)).to.be.revertedWith("Sender is a contract")
})
});
describe("Error deal test", () => {
it("assert test", async () => {
expect(await instance.assertTest(10)).to.be.true;
await expect(instance.assertTest(3)).to.be.reverted;
})
it("require test", async () => {
expect(await instance["requireTest(uint256)"](10)).to.be.true;
await expect(instance["requireTest(uint256)"](3)).to.be.reverted;
expect(await instance["requireTest(uint256,string)"](10,"less than 5")).to.be.true;
await expect(instance["requireTest(uint256,string)"](3,"less than 5")).to.be.revertedWith("less than 5");
})
it("revert test", async () => {
expect(await instance["revertTest(bool)"](false)).to.be.true;
await expect(instance["revertTest(bool)"](true)).to.be.reverted;
expect(await instance["revertTest(bool,string)"](false,"some reason")).to.be.true;
await expect(instance["revertTest(bool,string)"](true,"some reason")).to.be.revertedWith("some reason");
})
});
describe("Crypto test", () => {
it("keccak256 test", async () => {
let data = ethers.utils.toUtf8Bytes("hello world");
expect(await instance.keccak256Test(data)).to.be.equal(ethers.utils.keccak256(data))
});
it("sha256 test", async () => {
let data = ethers.utils.toUtf8Bytes("hello world");
expect(await instance.sha256Test(data)).to.be.equal(ethers.utils.sha256(data))
});
it("ripemd160 test", async () => {
let data = ethers.utils.toUtf8Bytes("hello world");
expect(await instance.ripemd160Test(data)).to.be.equal(ethers.utils.ripemd160(data))
});
it("ecrecover test", async () => {
let message = ethers.utils.solidityKeccak256(
["uint256","uint256[]","bytes"],
args
)
let messageHash = ethers.utils.hashMessage(ethers.utils.arrayify(message))
let signature = await user.signMessage(ethers.utils.arrayify(message))
const {r,s,v} = ethers.utils.splitSignature(signature)
expect(await instance.ecrecoverTest(messageHash,v,r,s)).to.be.equal(user.address)
});
});
describe("math operation test", () => {
it("add mod test", async () => {
let x = ethers.constants.MaxUint256;
let y = ethers.constants.Two;
let k = 10;
// x + y = 115792089237316195423570985008687907853269984665640564039457584007913129639937
expect(await instance.addmodTest(x,y,k)).to.be.equal(7);
});
it("mul mod test", async () => {
let x = ethers.constants.MaxUint256;
let y = ethers.constants.Two;
let k = 10;
// x * y = 231584178474632390847141970017375815706539969331281128078915168015826259279870
expect(await instance.mulmodTest(x,y,k)).to.be.equal(0);
});
});
describe("Inheritance test", () => {
it("this test", async () => {
expect(await instance.thisTest()).to.be.equal(instance.address)
});
it("thisCall test", async () => {
expect(await instance.thisCallTest()).to.be.equal(instance.address)
});
it("super test", async () => {
expect(await instance.superTest(5)).to.be.equal(3);
expect(await instance.superTest(13)).to.be.equal(13);
});
});
});
// GlobalVariables-02.js
const { expect, assert } = require("chai");
const { ethers } = require("hardhat");
const jsonData = require("../artifacts/contracts/GlobalVariables.sol/Base.json")
describe("GlobalVariables Test 01", function () {
let instance;
let owner,user,addrs;
let test_contract;
beforeEach( async function() {
const GlobalVariables = await ethers.getContractFactory("GlobalVariables");
instance = await GlobalVariables.deploy();
const NoReceiveEth = await ethers.getContractFactory("NoReceiveEth");
test_contract = await NoReceiveEth.deploy();
[owner, user, ...addrs] = await ethers.getSigners();
});
describe("Contract test", () => {
it("selfdestruct and balance test", async () => {
// 1 send ether
await instance.msgValueTest({
value:ethers.utils.parseEther("1.0")
});
expect(await instance.balanceTest()).to.be.equal(ethers.utils.parseEther("1.0"));
expect(await ethers.provider.getBalance(test_contract.address)).to.be.equal(0)
//transfer ether to test_contract will be failed
let transactionRequest = {
to:test_contract.address,
value:ethers.utils.parseEther("1.0")
}
await expect(owner.sendTransaction(transactionRequest)).to.be.reverted
expect(await ethers.provider.getBalance(test_contract.address)).to.be.equal(0)
// selfdestruct
await expect(instance.selfdestructTest(test_contract.address)).to.be.emit(instance,"SelfDestruct")
.withArgs(test_contract.address);
expect(await ethers.provider.getBalance(instance.address)).to.be.equal(0)
expect(await ethers.provider.getBalance(test_contract.address)).to.be.equal(ethers.utils.parseEther("1.0"))
await expect(instance.balanceTest()).to.be.reverted
});
it("code and code hash test", async () => {
let code = await instance.codeTest();
expect(await instance.codehashTest()).to.be.equal(ethers.utils.keccak256(code));
})
it("Send ether success", async () => {
// 1 send ether
await instance.msgValueTest({
value:ethers.utils.parseEther("1.0")
});
expect(await instance.balanceTest()).to.be.equal(ethers.utils.parseEther("1.0"));
await expect(instance.sendTest(user.address,ethers.utils.parseEther("0.6"))).to.
emit(instance,"SendEther").withArgs(user.address,ethers.utils.parseEther("0.6"),true)
})
it("Send ether failed", async () => {
await instance.msgValueTest({
value:ethers.utils.parseEther("1.0")
});
expect(await instance.balanceTest()).to.be.equal(ethers.utils.parseEther("1.0"));
await expect(instance.sendTest(test_contract.address,ethers.utils.parseEther("0.6"))).to.
emit(instance,"SendEther").withArgs(test_contract.address,ethers.utils.parseEther("0.6"),false)
})
it("transfer ether success", async () => {
// 1 send ether
await instance.msgValueTest({
value:ethers.utils.parseEther("1.0")
});
expect(await instance.balanceTest()).to.be.equal(ethers.utils.parseEther("1.0"));
await expect(instance.transferTest(user.address,ethers.utils.parseEther("0.6"))).to.
emit(instance,"SendEther").withArgs(user.address,ethers.utils.parseEther("0.6"),true)
})
it("transfer ether failed", async () => {
await instance.msgValueTest({
value:ethers.utils.parseEther("1.0")
});
expect(await instance.balanceTest()).to.be.equal(ethers.utils.parseEther("1.0"));
await expect(instance.transferTest(test_contract.address,ethers.utils.parseEther("0.6"))).to.
be.revertedWith("function selector was not recognized and there's no fallback nor receive function")
})
it("contractName test", async () => {
expect(await instance.contractNameTest()).to.be.equal("GlobalVariables")
})
it("creationCode and runtimeCode Test", async () => {
let crate_code = await instance.creationCodeTest()
assert.equal(crate_code,jsonData.bytecode)
let run_code = await instance.runtimeCodeTest()
assert.equal(run_code,jsonData.deployedBytecode)
});
it("interfaceId test", async () => {
expect(await instance.interfaceIdTest()).to.be.equal("0x80ac58cd")
});
it("min test", async () => {
expect(await instance.minTest()).to.be.equal(ethers.constants.MinInt256)
});
it("max test", async () => {
expect(await instance.maxTest()).to.be.equal(ethers.constants.MaxUint256)
});
});
});
有兴趣的小伙伴可以认真看一下单元测试内容。
单元测试结果运行npx hardhat coverage
,得到类似如下输出:
Version
=======
> solidity-coverage: v0.7.20
Instrumenting for coverage...
=============================
> GlobalVariables.sol
Compilation:
============
Compiled 1 Solidity file successfully
Network Info
============
> HardhatEVM: v2.9.1
> network: hardhat
GlobalVariables Test 01
Encode and Decode test
✔ Encode test (50ms)
✔ Decode test (44ms)
✔ EncodePacked test
✔ EncodeWithSelector test (39ms)
✔ EncodeWithSignature test (38ms)
.......
Crypto test
✔ keccak256 test
✔ sha256 test
✔ ripemd160 test
✔ ecrecover test
math operation test
✔ add mod test
✔ mul mod test
Inheritance test
✔ this test
✔ thisCall test
✔ super test
GlobalVariables Test 01
Contract test
✔ selfdestruct and balance test (52ms)
✔ code and code hash test (98ms)
✔ Send ether success
✔ Send ether failed
✔ transfer ether success
✔ transfer ether failed
✔ contractName test
✔ creationCode and runtimeCode Test
✔ interfaceId test
✔ min test
✔ max test
45 passing (2s)
----------------------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
----------------------|----------|----------|----------|----------|----------------|
contracts/ | 100 | 100 | 100 | 100 | |
GlobalVariables.sol | 100 | 100 | 100 | 100 | |
----------------------|----------|----------|----------|----------|----------------|
All files | 100 | 100 | 100 | 100 | |
----------------------|----------|----------|----------|----------|----------------|
> Istanbul reports written to ./coverage/ and ./coverage.json