Solidity随笔
TIPS:
常用命令
solcjs --include-path node_modules/ --include-path contracts/libraries --include-path interfaces --base-path .-o. --abi contracts/A3SQueue.sol
~/gopath/bin/abigen --bin=OneHiController_sol_OneHiController.bin --abi=OneHiController_sol_OneHiController.abi --pkg=oneHi --out=one_hi_controller.go
想调用某个合约的方法,找ABI时可以试试到区块浏览器找。
一、solidity基本特性
1、this和msg.sender的区别
this指的是合约本身,msg.sender指的是合约的调用者。
2、pure和view的区别
都是用于修饰函数,当函数不读不改状态变量时用pure
当函数只读不改状态变量时用view
3、在一个合约中可以直接调用另一个合约的方法。
方法是:首先写出要调用的合约(例如夺宝调用Fracton,被调用合约是Fracton)的对应接口,里面定义方法名和参数、返回值。
然后在主合约中(如夺宝)的构造方法内把被调用合约的地址放到本地状态变量中,import被调用合约,然后接口名(合约地址)实例化该被调用合约,就可以调用这个合约的方法了。
address fftAddr = IFractonSwap(fractonSwapAddr).miniNFTtoFFT(miniNftAddr);
4、public和external的区别
public修饰的变量和函数,任何用户或者合约都能调用和访问。
private修饰的变量和函数,只能在其所在的合约中调用和访问,即使是其子合约也没有权限访问。
internal 和 private 类似,不过, 如果某个合约继承自其父合约,这个合约即可以访问父合约中定义的“内部”函数。
external 与public 类似,只不过这些函数只能在合约之外调用 - 它们不能被合约内的其他函数调用。
5、memory关键字
memory可直接理解为"内存",用于存储临时的变量,是相对于状态变量而言的,费用更便宜一些。
6、type关键字
type是关键字,但目前已知的用法只有:
type(contractName).creationCode;
type(int256).max
type(int256).min
7、abi是什么?abi.encode和abi.encodePacked是什么?
abi可以理解为接口文档,encode和encodePacked可以将若干个参数以任意顺序编码为bytecode,区别是encode会将每个参数编码为32字节并补0,而encodePacked不会补0。例如openZeppelin的工具utils/Create2 使用时传入的第三个参数,需要bytecode类型,虽然这两个方法参数的顺序可以任意,但为了输出的结果能被Create2使用,还是得按Create2要的那个顺序来。
8、状态变量加上public修饰符,编译器自动生成一个getter函数
9、mapping是无法被遍历的,想遍历mapping必须配合一个数组切片
10、如何创建合约?
可以通过关键字new或者调用内置函数create2,其中create2是加盐的,可以推导出合约地址。
11、函数的返回值
函数可以有多个返回值。
函数的返回值如果定义了变量名,那么可以在函数体中赋值,然后省略return语句。
12、修饰符可以用于加锁
uint private unlocked = 1;modifier lock() {require(unlocked == 1, 'UniswapV2: LOCKED');unlocked = 0;_;unlocked = 1;}
//使用时:
function mint(address to) external lock returns (uint liquidity) {
//...
}
13、keccak256 是比SHA-256更优越的一种哈希函数
14、什么是4字节函数选择器?
"内置函数"的更严谨的说法应该是“全局变量”
abi.encodeWithSelector是将4字节函数选择器和参数进行编码。
参考:Solidity极简入门: 29. 函数选择器Selector
4字节函数选择器就是把函数签名(函数名+参数名)编码为4个字节的字节码
可用于选择调用的函数
参考:简书-Solidity Call函数
这里的token.call是底层的合约调用方法,不建议用。
推荐的方法是通过接口实例化后调用。
bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));function _safeTransfer(address token, address to, uint value) private {(bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');}
15、取时间戳的最佳实践
原因:时间戳是uint256,只需要保留最低的32位即可,但不可直接强转。
好处:节省gas。
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
16、assembly关键字的作用是?
uniswap v2有这么一段代码:
function createPair(address tokenA, address tokenB) external returns (address pair) {//...assembly {pair := create2(0, add(bytecode, 32), mload(bytecode), salt)}//...}
查文档得知assembly为solidity内置的内联汇编语言。
这段代码是很底层的了,目前的开发者已经不需要再写这种代码,因为OpenZeppelin的utils/Create2已经封装了这个代码到deploy方法中。
17、solidity继承的父子合约,其构造函数的执行顺序?
contract TestToken is ERC20{}
如果在TestToken的构造函数后面给ERC20传参了,是先执行ERC20构造函数再执行TestToken构造函数.
如果没传参,会编译报错,编译器会要求开发者把TestToken改为抽象。
如果父合约的构造函数不需要参数,那么子合约也不需要往父合约构造函数中传参。
// contracts/OurToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;import "@openzeppelin/contracts/token/ERC20/ERC20.sol";contract OurToken is ERC20 {constructor(uint256 initialSupply) ERC20("OurToken", "OT") {_mint(msg.sender, initialSupply);}
}
二、OpenZeppelin特性
1、OpenZeppelin的常用功能:
1.1、access/Ownable
access/Ownable 主要是提供了一个onlyOwner修饰符
继承Ownable的合约内部的方法,只要使用了onlyOwner修饰符,都会校验方法的调用者是否owner
Ownable源代码
1.2、token/ERC20
ERC20是以太坊上最基本和常用的一种代币合约标准。
此目录下有ERC20.sol和IERC20.sol
如果是发一个ERC20代币直接继承ERC20即可,如果是想在合约中调用某个代币的方法,则需要import IERC20.sol,然后将该代币合约实例化后调用。
例如夺宝项目中:
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";function _swapNFT() internal {//...IERC20(hiToken).approve(swapAddr, targetAmount*1e18);//...}function _splitProfit() internal returns(uint256, uint256) {//...uint256 balance = IERC20(hiToken).balanceOf(address(this)); //...IERC20(hiToken).transfer(maker, amountOfMaker);//...}
ERC20.sol中实现了transfer、balanceOf、approve等方法,提供internal的_mint _burn 方法。
1.3、token/ERC721
ERC721是以太坊的NFT标准。用法类似ERC20章节。
NFT是非同质化代币的意思,mint出来的每个代币都是与众不同的。
在其元数据MetaData内有着自己独特的属性。
每个NFT代币都会有一个tokenId
ERC721内部会有一个mapping记录owner和tokenId的映射关系。
NFT可以在不同的地址之间转移transfer。
1.4、token/ERC1155
ERC1155既是ERC20又是ERC721,且可以批量处理转账、查询等功能。
IERC1155(miniNFT).setApprovalForAll(swapAddr, true);
1.5、utils/Create2
常用deploy方法,封装了创建合约的assembly语句create2(…)
用于部署合约
//源码
function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) internal returns (address addr) {require(address(this).balance >= amount, "Create2: insufficient balance");require(bytecode.length != 0, "Create2: bytecode length is zero");/// @solidity memory-safe-assemblyassembly {//这段和uniswap-v2-core:createPair中使用的是一样的addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt)}require(addr != address(0), "Create2: Failed on deploy");}
示例:
//create table方法内部:address tableAddr = Create2.deploy(0,salt,TableHelper.getBytecode(address(this), fftAddr, targetAmount, msg.sender));//function getBytecode(address controllerAddr, address fftAddr, uint256 targetAmount,address makerAddr) public pure returns (bytes memory) {bytes memory bytecode = type(OneHiTable).creationCode;//这里调用abi.encodePacked传入创建合约的bytecode和参数bytecode//是Create2方法要求的,参数bytecode的生成需要使用abi.encode方法return abi.encodePacked(bytecode, abi.encode(controllerAddr, fftAddr, targetAmount,makerAddr));}
1.6、utils/Address
一般使用这个库的话需要using Address for address;或Address.xxx(address)
看夺宝项目的代码时发现导入了这个库但代码中没有用到,这种情况虽然不影响编译、部署和运行的正常进行,但会影响到部署gas费。
2、可升级合约
2.1、 可升级合约的底层原理
假设有两个合约ContractA和ContractB,A引入了B,A的函数func1调用了B的func2,对B的数据var_b进行了操作。这个是正常的调用流程。其底层使用了CALL
假设A有数据var_a,现在需要让A使用B的函数func2对A的数据var_a做修改,那么需要使用的底层调用源码是DELEGATECALL - 委托调用。
可升级合约基于DELEGATECALL - 委托调用实现。
可升级合约的实现需要两种合约角色:
代理合约proxy contract + 实现合约implement contract。
代理合约中保存了数据且其合约地址永久不变。
实现合约中写了对数据增删改查的逻辑,供代理合约调用。
升级就是代理合约使用了新的实现合约。
2.2、OpenZeppelin可升级合约实操
OpenZeppelin提供了一整套的可升级合约库和工具包,开箱即用,大致分为以下三步:
1、写实现合约代码,继承OpenZeppelin的XxxUpgradeable库
代码中不要写构造函数,改为普通的命名为initialize的函数,以initializer为修饰符。
initialize函数内应该依次调用各个Upgradeable的库的init函数以初始化。
如果是UUPS类型的实现合约,那么应该把升级相关代码也实现出来。
2、使用OpenZeppelin的可升级合约工具包部署实现合约,该工具会自动生成代理合约并一起部署。
3、升级:写新的实现合约的代码并使用工具包部署。
2.3、参考
智能合约升级原理1:起源
三、hardhat的使用
1、初始化项目的命令
如果是新创建项目,运行:
yarn add --dev hardhat
yarn hardhat init
如果是拉取了别人的项目,会提示某些文件已经存在,则运行
yarn add --dev hardhat
yarn hardhat ***
//任意命令都会提示创建,给了一些选项,此时选择生成空白的hardhat.config.js
✔ What do you want to do? · Create an empty hardhat.config.js
✨ Config file created ✨
2、跑本地节点和部署合约、调用合约方法的命令
yarn hardhat node
注意,此节点会持续运行,除非Ctrl+c退出
直接运行yarn hardhat run scripts/*.js是启动自带节点,运行完节点也就关掉了。
想运行到本地持久节点上,需要加参数–network localhost
yarn hardhat run --network localhost scripts/index.js
3、如果项目使用Typescript如何运行
应下载typescript相关依赖并把hardhat.config.js改为hardhat.config.ts
//下载相关依赖
yarn add --dev ts-node typescript
yarn add --dev chai @types/node @types/mocha @types/chai
四、Uniswap v2 代码解读
1、测试文件是如何跑通的?
拉取项目到本地后,先尝试按hardhat项目编译和跑测试发现不行,遇到了各种报错。最后看官方文档发现,一是不需要hardhat,二是原来的依赖版本有很多已经升级了,一些过时的代码导致了报错。
解决办法是:
a.首先删除yarn.lock文件
b.修改package.json文件中的依赖,增加命令
c.yarn命令更新依赖
d.把测试文件中相关代码更新为最新的写法
e.运行命令:yarn test:evm
详见github.com/ScopeLift/ovm-uniswap-v2-core
2、参考
深入理解 Uniswap v2 合约代码
https://mirror.xyz/adshao.eth/VY6aLzdjwXGif9O1C7UMuYFmivC4q5jDQqQUho6tLWY
Uniswap V2 core源码解析:
https://juejin.cn/post/7185379590162874429
3、合约测试技术栈都有哪些?
Mocha 、chai、waffle
3.1 首先是mocha:
mocha是测试框架,提供了mocha命令。
可配置于package.json:
"test:evm": "yarn compile && mocha",
describe
it(‘should be correct’,async()=>{…})//测试单元,是mocha提供的
3.2 然后是chai:
提供了断言语句
写法有三种风格,常用expect语句
//测试方法返回值符合预期:
expect(await contractObject.doSomething()).to.eq(targetValue)
//如果想测试事件则这么写:
expect(await contractObject.doSomething())
.to.emit(contractObject,'EventName')
.withArgs(eventParam1,eventParam2,eventParam3)
//断言某种调用会失败:
it('transfer:fail', async () => {await expect(token.transfer(other.address, TOTAL_SUPPLY.add(1))).to.be.reverted await expect(token.connect(other).transfer(wallet.address, 1)).to.be.reverted })
3.3 最后是waffle
用于提供本地区块链环境,部署合约到本地,生成合约对象。
const provider = new MockProvider({ganacheOptions:{hardfork: 'istanbul',mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn',gasLimit: 9999999}})const [wallet, other] = provider.getWallets()let token: ContractbeforeEach(async () => {//beforeEach是mocha提供的钩子,在每个测试单元运行之前运行。token = await deployContract(wallet, ERC20, [TOTAL_SUPPLY])})
4、为什么swap方法中没有校验转入金额就直接开始执行转出token的操作了?
一笔交易可以理解为一个事务,具有原子性,要么全成功,要么全失败,虽然在swap方法内首先执行了转出,但如果后面的校验转入不成功,那么连带着转出操作也不会成功。
一笔交易中可以对多个函数进行调用,同样的,这些函数的操作同处于一个事务中,只要有一个函数失败了,那么交易就会失败。