Solidity知识点汇总
前言
本方将记录我学习solidity过程中的一些知识点。随着学习的深入,内容会持续更新。
学习过程中参考的技术文档:
solidity
solidity-by-example
以太坊电子书
hardhat
ethers.js
三个特殊函数
- constructor: 构造函数,仅在合约部署时执行一次,当指定为payable时,表示允许部署时向合约转以太坊;
- receive: 用于接收转账,外部调用calldata参数为空,且msg.value不为空时,该函数被默认调用,该函数的可见性一般定义为external,但必须指定为payable;
- fallback: 当外部调用指定的函数签名不匹配当前合约的所有函数时或calldata为空时,默认调用该函数,一般用于错误处理。可见性必须定义为external,当该函数指定为payable时可以替代receive函数;
注:这些特殊函数定义时都不需要指定function关键词;
三种变量类型
- memory: 这种类型的变量叫临时变量,其生命周期仅存在于函数执行时,函数执行结束后这种类型的变量被销毁。
- calldata: 这种类型也是一种临时变量。EOA调用合约方法或合约间方法调用时,传递到方法的参数即是这种类型,这种类型的数据只可以读取,不能修改,如果要修改只能复制到memory类型变量进而对memory类型进行修改。当外部调用传递大量数据时这种将函数的参数定义为calldata相当有用,因为它减少了实参数据到memory的复制过程,从而节省了gas消耗。
- sotrage: 永久的存在区块链的,其成本比较高昂。
函数的四种修饰符
- view: 函数内部只能读取合约中定义的状态变量,而不修改;
- prue: 函数内部即不能读取也不能修改合约中定义的状态变量;
- payable: 外部调用该类型函数时允许发送ether;
- nonpaybe: 外部调用该类型函数时不允许发送ether,当外部调用该类型的方法进行ether转账时,交易会失败扣除相应gas费用后ehter会返回给调用方;
注:如果没有payable修饰符,当外部调用者调用合约方法时误将ether转入合约,而合约未设置账户进取功能,那么这笔ether将被锁定无法提取。
this
- 以太坊合约中this可以显式转换为address类型,转换后可以调用address的所有方法,如:合约余额address(this).balance;
- this还可以用于调用合约自身的方法,如下
import "hardhat/console.sol";
contract Example() {
function a() public view {
console.log( this.b() ); // 3
}
function b() public pure returns(uint) {
return 3;
}
}
selfdestruct
当合约代码不再使用时可以调用该函数销毁合约,被销毁的合约将从区块链中删除。确保删除后不会再有其它调用者向该合约地址转账,销毁后的所有转账将会被冻结。该函数调用时接收payable地址,用于接收该合约中剩余ether。
contract Contract {
uint _countdown = 10;
constructor() payable { }
function tick() public {
_countdown--;
if(_countdown == 0) {
// NOTE: we must cast to payable here
// some solidity methods protect
// against accidentally sending ether
selfdestruct(payable(msg.sender));
}
}
}
异常
触发机制
- send函数会向调用者抛出异常,该异常会在调用链传递,直到有try/catch对其进行捕获;
- 底层函数call/delegatecall/staticcall返回的第一个值用于标识执行结果,如果为false则表明调用异常;
注:作为EVM设计的一部分,如果调用的账户不存在则其返回的第一个值是true,因此调用底层函数时需要先检查账户是否存在。
错误类型
- Panic(uint256),该类型的错误一般应用于底层,一般编译器可以检查出来;
- Error(string),该类型的错误一般应用于业务逻辑条件判断;
抛出错误的方法
- assert:产生Panic(uint)类错误
- require:产生Error(string)类错误
- revert:产生Error(string)类错误,并且支持定制类错误CustomError()
更详细的错误解析看这里
合约调用
三方库调用
使用ethers.js或web3.js通过JSON-RPC调用合约方法。
- 编写合约并部署
// Switch.sol
contract Switch {
bool isOn;
function change(bool _isOn) external {
isOn = _isOn;
}
}
- 根据合约ABI及合约地址调用合约
// turnOnSwitch.js
require('dotenv').config();
const ethers = require('ethers');
//此处用合约生成的ABI
const contractABI = [];
//根据本地配置来初始化provider
const provider = new ethers.providers.AlchemyProvider(
'sepolia',
process.env.TESTNET_ALCHEMY_KEY
);
const wallet = new ethers.Wallet(process.env.TESTNET_PRIVATE_KEY, provider);
async function main() {
const counterContract = new ethers.Contract(
'合约地址',
contractABI,
wallet
);
await counterContract.change(true);
}
main();
合约间调用
通常EOA通过发送交易的形式与合约交互,其中交易中有个字段data用于指明要调用 的目标合约的方法及参数,该字段也就是我们常说的calldata。不同合约间方法调用通常也需要传递calldata。合约方法调用一般有如下两种情形:
- 调用者组织calldata。
//通过abi.encodeWithSignature方法生成calldata
contract Example {
function sendData(address x) external {
(bool s, ) = x.call(
abi.encodeWithSignature("receiveData(uint256)", 5)
);
require(s);
}
}
- 通过接口定义来调用
interface A {
function receiveData(uint) external;
}
contract Example {
function sendData(address x) external {
(bool s, ) = A(x).receiveData(5);
require(s);
}
}
hardhat开发流程
hardhat是一个工具集,其中囊括了智能合约开发过程中所涉及的一切,包括合约的编译、部署、测试和调试等。
hardhat有如下特点:
- 本地测试,包括本地网络设置;
- Solidity编译及错误检查;
- 与其它工具、插件的集成,比如:ethers.js、Mocha等;
- 以更直观的方式让开发人员与智能合约交互;
下面以代码示例说明hardhat开发、部署、测试等过程
项目依赖安装
npm install --save-dev hardhat
npm install @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers dotenv
上述dotenv主要用于解析本地环境变量,一般项目的最佳实践是将一些敏感信息以环境变量的形式提供。
初始化项目
npx hardhat init
初始化后项目目录结构如下:
- 创建.env文件,将一些敏感信息以环境变量形式存储,如:
编辑相关文件
- 编辑配置文件hardhat.config.js
require("@nomiclabs/hardhat-waffle");
require("dotenv").config(); //此处解析了本地.env文件中的环境变量
module.exports = {
solidity: "0.8.4",
networks: {
sepolia: {
url: process.env.SEPOLIA_URL,
accounts: [process.env.PRIVATE_KEY]
},
}
};
- 在contracts目录下添加合约文件Faucet.sol
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
contract Faucet {
function withdraw(uint _amount) public {
// users can only withdraw .1 ETH at a time, feel free to change this!
require(_amount <= 100000000000000000);
payable(msg.sender).transfer(_amount);
}
// fallback function
receive() external payable {}
}
- 在scripts目录下添加部署脚本deploy.js
const ethers = require('ethers');
require('dotenv').config();
async function main() {
const url = process.env.SEPOLIA_URL;
let artifacts = await hre.artifacts.readArtifact("Faucet");
const provider = new ethers.providers.JsonRpcProvider(url);
let privateKey = process.env.PRIVATE_KEY;
let wallet = new ethers.Wallet(privateKey, provider);
// Create an instance of a Faucet Factory
let factory = new ethers.ContractFactory(artifacts.abi, artifacts.bytecode, wallet);
let faucet = await factory.deploy();
console.log("Faucet address:", faucet.address);
await faucet.deployed();
}
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});
编译合约
npx hardhat compile
注:合约编译完成后会在工程目录下生成artifacts目录,其中存放了合约编译后的ABI及bytecode
部署合约到指定网络
//hardhat默认会在内存中生成一个本地网络,如果不指定--network则连接本地网络
//此处--network指定要将合约部署到那个网络,相关的网络配置在hardhat.config.js中,比如此处我们配置的sepolia
npx hardhat run scripts/deploy.js --network sepolia
注:部署完成后我们输出了合约地址。
测试
在tests目录下存放测试安例。如下 :
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { expect } = require('chai');
describe('Faucet', function () {
// We define a fixture to reuse the same setup in every test.
// We use loadFixture to run this setup once, snapshot that state,
// and reset Hardhat Network to that snapshot in every test.
async function deployContractAndSetVariables() {
const Faucet = await ethers.getContractFactory('Faucet');
const faucet = await Faucet.deploy();
const [owner] = await ethers.getSigners();
console.log('Signer 1 address: ', owner.address);
return { faucet, owner };
}
it('should deploy and set the owner correctly', async function () {
const { faucet, owner } = await loadFixture(deployContractAndSetVariables);
expect(await faucet.owner()).to.equal(owner.address);
});
});
执行测试:
npx hardhat test
通过脚本调用合约方法
// add the game address here and update the contract name if necessary
const contractAddr = "合约地址";
const contractName = "Faucet";
async function main() {
// attach to the game
const contract= await hre.ethers.getContractAt(contractName, contractAddr );
const tx = await contract.withdraw(20000);
const receipt = await tx.wait();
console.log(receipt);
}
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});