首页
Search
1
yamux: how to work?
79 阅读
2
The Art of Memory Allocation: Malloc, Slab, C++ STL, and GoLang Memory Allocation
70 阅读
3
How to receive a network packet in Linux
63 阅读
4
Maps and Memory Leaks in Go
54 阅读
5
C++ redis connection pool
52 阅读
测试
Wireguard
K8s
Redis
C++
Golang
Libcurl
Tailscale
Nginx
Linux
web3
Uniswap V2
Uniswap V3
EVM
security
solidity
openzeppelin
登录
Search
标签搜索
web3
solidity
web3 security
c++
uniswapV3
redis
evm
uniswap
性能测试
k8s
wireguard
CNI
http
tailscale
nginx
linux
设计模式
Jericho
累计撰写
51
篇文章
累计收到
13
条评论
首页
栏目
测试
Wireguard
K8s
Redis
C++
Golang
Libcurl
Tailscale
Nginx
Linux
web3
Uniswap V2
Uniswap V3
EVM
security
solidity
openzeppelin
页面
搜索到
29
篇与
的结果
2025-05-24
About the EVM Part III
13.Return指令🚀 1. RETURN 指令✅ 功能终止执行,并返回内存中一段数据作为调用结果。🔍 实现源码路径:go-ethereum/core/vm/instructions.gofunc opReturn(pc *uint64, evm *EVM, contract *Contract, stack *Stack) ([]byte, error) { offset, size := stack.pop(), stack.pop() ret := contract.GetMem(int64(offset.Uint64()), int64(size.Uint64())) return ret, nil }📦 Gas 消耗指令本身消耗:0(RETURN 没有额外指令成本)访问 memory 的成本:根据 offset+size 的访问范围来收取内存扩展费用(memoryGasCost)func memoryGasCost(memSize uint64) uint64 { // linear + quadratic cost function // linear: memSize * 3 // quadratic: (memSize^2) / 512 }💡 特点调用成功返回数据(用于 CALL, DELEGATECALL 等)常用于函数正常返回🚨 2. REVERT 指令✅ 功能回滚状态,同时返回错误数据(不会消耗所有 gas)。🔍 实现func opRevert(pc *uint64, evm *EVM, contract *Contract, stack *Stack) ([]byte, error) { offset, size := stack.pop(), stack.pop() ret := contract.GetMem(int64(offset.Uint64()), int64(size.Uint64())) evm.interpreter.returnData = ret return ret, ErrExecutionReverted }📦 Gas 消耗指令本身:0内存访问费用:同样受 memoryGasCost 限制保留剩余 gas,不销毁💡 特点引入自 Byzantium(EIP-140)与 INVALID、ASSERT 不同,它会“温柔地失败”,gas 不全耗尽,便于合约进行 try/catch💣 3. SELFDESTRUCT 指令(旧称 SUICIDE)✅ 功能销毁当前合约,并将余额发送给指定地址。🔍 实现func opSelfDestruct(pc *uint64, evm *EVM, contract *Contract, stack *Stack) ([]byte, error) { beneficiary := common.Address(stack.pop().Bytes20()) // handle refund and deletion evm.StateDB.SelfDestruct(contract.Address()) // transfer balance to beneficiary evm.Transfer(contract.Gas, contract.Address(), beneficiary, contract.Balance()) contract.SelfDestruct = true return nil, nil }📦 Gas 消耗固定成本:5000(EIP-150 后)若首次销毁:+24000(删除合约 refund)若 beneficiary 是新地址:再加 25000// In gas_table.go (EIP150): SelfDestruct: 5000, SelfDestructRefund: 24000,💡 特点合约立即从状态树中移除(实际上是标记为删除,block 结束后才真正移除)balance 强制转移,但无法返回自定义数据(不是直接返回 data)🔁 总结对比表指令名功能描述返回数据Gas 行为出现版本RETURN正常返回执行结果✅memory + 0初始版本REVERT回滚并返回错误信息✅memory + 0Byzantium (EIP-140)SELFDESTRUCT销毁合约,发送余额❌高消耗+可能 refund初始版本(EIP-6 优化)14.CALL、DELEGATECALL、STATICCALL📘 总览指令名是否改变调用者是否转移 msg.value是否可变更 storageCALL✅ 改变 msg.sender✅ 支持转账✅ 支持状态更改DELEGATECALL❌ 保持调用上下文❌ 不支持转账✅ 支持状态更改STATICCALL❌ 保持调用上下文❌ 不支持转账❌ 禁止状态更改✅ 1. opCall —— 普通合约调用核心逻辑ret, returnGas, err := interpreter.evm.Call( scope.Contract.Address(), // caller toAddr, // callee args, // calldata gas, // gas &value, // msg.value )实际调用函数:func (evm *EVM) Callcode(caller Address, addr Address, input []byte, gas uint64, value *big.Int) ([]byte, uint64, error)实现入口:core/vm/evm.go func (evm *EVM) Call(...) { return evm.call(...) // 里面处理合约创建、状态快照、gas 计算、revert 回滚等 }✅ 2. opDelegateCall —— 保留 caller/value 的“委托调用”核心逻辑ret, returnGas, err := interpreter.evm.DelegateCall( scope.Contract.Caller(), // 委托原始 caller scope.Contract.Address(), // 委托当前调用合约 toAddr, // 被委托的目标合约 args, gas, scope.Contract.value, // 保留 value )实际函数:func (evm *EVM) DelegateCall(caller, address Address, codeAddr Address, input []byte, gas uint64, value *big.Int) ([]byte, uint64, error)在 call.go 中构造新的 Contract 时,会设置:contract.CallerAddress = caller (保留 msg.sender)contract.value = value (保留 msg.value)使用被调用者 toAddr 的代码✅ 3. opStaticCall —— 只读调用核心逻辑ret, returnGas, err := interpreter.evm.StaticCall( scope.Contract.Address(), // caller toAddr, // callee args, gas, )实际函数:func (evm *EVM) StaticCall(caller, addr Address, input []byte, gas uint64) ([]byte, uint64, error)StaticCall 会在创建子合约时加上只读标记 contract.SetReadOnly(true),任何写状态的行为将触发 ErrWriteProtection 错误。⛽️ 四、Gas 消耗机制Gas 在 EVM 中高度复杂,调用相关的 gas 计算规则如下(均实现于 core/vm/gas_table.go 与 core/vm/gas.go):1. 通用机制在这几条指令中,gas 的计算由 interpreter.evm.callGasTemp 暂存,实际 gas 是前置算好的,计算时考虑以下因素:项描述BaseCost固定开销(通常为 700)Transfer如果转账了 value,增加 CallStipend = 2300(用于 fallback gas)MemoryGas计算传入传出数据的 memory access 成本(线性+平方增长)ColdAccessEIP-2929 引入冷账户调用要额外加 gas2. 详细举例:CALLgas = baseGas + memoryCost + (isColdAccount ? ColdAccessCost : 0) if value != 0 { gas += CallStipend }3. STATICCALL 特别限制:不允许非零 value(否则报错)状态写入全部禁止(storage、selfdestruct 等都会报 ErrWriteProtection)15.create、create2CREATE指令CREATE指令是EVM中用于创建新合约的基本指令,它使用调用者的地址和nonce作为合约地址的计算因子。在go-ethereum中,CREATE指令的实现主要在core/vm/instructions.go文件中:opCreate函数分析主要逻辑流程权限检查:if interpreter.readOnly { return nil, ErrWriteProtection }检查EVM是否处于只读模式,如果是则不允许创建合约。参数获取:value := scope.Stack.pop() // 转账金额 offset, size := scope.Stack.pop(), scope.Stack.pop() // 内存位置和大小 input := scope.Memory.GetCopy(offset.Uint64(), size.Uint64()) // 初始化代码 gas := scope.Contract.Gas // 可用gasEIP150调整:if interpreter.evm.chainRules.IsEIP150 { gas -= gas / 64 }根据EIP150规则保留1/64的gas以防调用深度过大。执行创建:res, addr, returnGas, suberr := interpreter.evm.Create(scope.Contract.Address(), input, gas, &value)调用底层Create方法实际执行合约创建。结果处理:根据不同链规则处理ErrCodeStoreOutOfGas错误将结果地址压入栈中处理剩余的gas返还返回数据设置:如果执行被revert,保存返回数据否则清空返回数据缓冲区关键点使用调用者地址和nonce计算新合约地址处理Homestead和Frontier规则下的不同错误处理逻辑实现了EIP150的gas调整规则将创建结果(地址或错误)压入堆栈CREATE指令的gas消耗包括以下几个部分:基础创建费用:32,000 gas (params.CreateGas)内存扩展费用:如果操作需要扩展内存,按内存扩展规则计算代码存储费用:200 gas/字节(合约部署后存储的字节码)初始化代码执行费用:执行初始化代码时消耗的gasCREATE2指令CREATE2是EVM中更灵活的合约创建指令,允许调用者控制合约地址的计算方式,通过添加"salt"参数使得地址不依赖于nonce。Golang实现同样在core/vm/instructions.go中:opCreate2函数分析主要逻辑流程权限检查:if interpreter.readOnly { return nil, ErrWriteProtection }同样检查是否只读模式。参数获取:endowment := scope.Stack.pop() // 转账金额 offset, size := scope.Stack.pop(), scope.Stack.pop() // 内存位置和大小 salt := scope.Stack.pop() // 盐值 input := scope.Memory.GetCopy(offset.Uint64(), size.Uint64()) // 初始化代码 gas := scope.Contract.Gas // 可用gasEIP150调整:gas -= gas / 64同样保留1/64的gas。执行创建:res, addr, returnGas, suberr := interpreter.evm.Create2(scope.Contract.Address(), input, gas, &endowment, &salt)调用Create2方法,传入盐值。结果处理:如果有错误清空栈值否则将地址压入栈中处理剩余的gas返还返回数据设置:处理revert情况下的返回数据关键点使用盐值(salt)参与地址计算,使地址可预测不需要nonce,地址计算方式为keccak256(0xff ++ sender ++ salt ++ keccak256(init_code))[12:]错误处理比CREATE简单,不区分特殊错误情况同样实现EIP150的gas调整规则CREATE2的Gas消耗CREATE2的gas消耗与CREATE类似,但有以下区别:基础创建费用:32,000 gas (与CREATE相同)额外的哈希计算费用:CREATE2需要计算Keccak256哈希,每次哈希计算需要支付30 gas内存扩展费用:与CREATE相同代码存储费用:200 gas/字节(与CREATE相同)初始化代码执行费用:与CREATE相同关键区别地址计算方式:CREATE: keccak256(rlp([sender, nonce]))[12:]CREATE2: keccak256(0xff ++ sender ++ salt ++ keccak256(init_code))[12:]可预测性:CREATE地址依赖于nonce,无法提前预测CREATE2地址可以通过salt和init_code预先计算重放保护:CREATE自动增加nonce防止重放CREATE2需要应用层通过salt防止重放最新变化在最新的以太坊升级中(如伦敦升级、柏林升级等),CREATE和CREATE2的gas计算有一些调整:EIP-2929:增加了状态访问的gas成本,影响了合约创建时的状态访问操作EIP-3860:对init code长度收取额外gas,限制过长的init code这些变化使得合约创建的gas计算更加复杂,但基本框架保持不变。实际应用建议如果需要可预测的合约地址(如状态通道、合约工厂等),使用CREATE2如果不需要地址预测,使用CREATE更简单在gas估算时,一定要考虑初始化代码的执行成本和最终的代码存储成本对于复杂的合约部署,可以考虑使用代理模式减少部署成本以上代码和分析基于go-ethereum最新稳定版本,具体实现可能会随着版本更新而变化。16.GAS在EVM中,交易和执行智能合约需要消耗计算资源。为了防止用户恶意的滥用网络资源和补偿验证者所消耗的计算能源,以太坊引入了一种称为Gas的计费机制,使每一笔交易都有一个关联的成本。在发起交易时,用户设定一个最大Gas数量(gasLimit)和每单位Gas的价格(gasPrice)。如果交易执行超出了gasLimit,交易会回滚,但已消耗的Gas不会退还。以太坊上的Gas用gwei衡量,它是ETH的子单位,1 ETH = 10^9 gwei。一笔交易的Gas成本等于每单位gas价格乘以交易的gas消耗,即gasPrice * gasUsed。gas价格会随着时间的推移而变化,具体取决于当前对区块空间的需求。gas消耗由很多因素决定,并且每个以太坊版本都会有所改动,下面总结下:calldata大小:calldata中的每个字节都需要花费gas,交易数据的大小越大,gas消耗就越高。calldata每个零字节花费4 Gas,每个非零字节花费16 Gas(伊斯坦布尔硬分叉之前为 64 个)。内在gas:每笔交易的内在成本为21000 Gas。除了交易成本之外,创建合约还需要 32000 Gas。该成本是在任何操作码执行之前从交易中支付的。opcode固定成本:每个操作码在执行时都有固定的成本,以Gas为单位。对于所有执行,该成本都是相同的。比如每个ADD指令消耗3 Gas。opcode动态成本:一些指令消耗更多的计算资源取决于其参数。因此,除了固定成本之外,这些指令还具有动态成本。比如SHA3指令消耗的Gas随参数长度增长。内存拓展成本:在EVM中,合约可以使用操作码访问内存。当首次访问特定偏移量的内存(读取或写入)时,内存可能会触发扩展,产生gas消耗。比如MLOAD或RETURN。访问集成本:对于每个外部交易,EVM会定义一个访问集,记录交易过程中访问过的合约地址和存储槽(slot)。访问成本根据数据是否已经被访问过(热)或是首次被访问(冷)而有所不同。Gas退款:SSTORE的一些操作(比如清除存储)可以触发Gas退款。退款会在交易结束时执行,上限为总Gas消耗的20%(从伦敦硬分叉开始)。GAS 指令的功能作用:将当前合约调用剩余的 Gas 压入操作数栈,供后续指令使用。操作码:0x5A(参见 EVM 操作码列表)。Gas 消耗:固定 2 Gas(基础成本)。opGas 函数实现解析func opGas(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { scope.Stack.push(new(uint256.Int).SetUint64(scope.Contract.Gas)) return nil, nil }关键组件:scope.Contract.Gas当前合约调用的剩余 Gas 值,存储在 Contract 结构体中,随指令执行动态更新。scope.Stack.push将数据压入 EVM 的操作数栈(后进先出结构),栈的每个槽位为 uint256 类型。uint256.Int以太坊自定义的 256 位无符号整数库,用于处理 EVM 的 256 位数据宽度。执行流程:读取剩余 Gas:从 scope.Contract.Gas 获取当前剩余 Gas。转换为 uint256:通过 SetUint64 将 Gas 值封装为 256 位整数。压入栈顶:调用 scope.Stack.push 将 Gas 值存入栈中。返回空结果:GAS 指令不修改内存或存储,故返回 nil。EVM 执行上下文与 Gas 管理Gas 的生命周期交易初始化:用户为交易设置 GasLimit,例如 21000 Gas(简单转账)。合约执行:每条指令消耗 Gas(如 ADD 消耗 3 Gas,SSTORE 动态计算)。动态扣除:执行指令前,EVM 检查剩余 Gas 是否足够,不足则回滚。// 伪代码:EVM 执行指令前的 Gas 检查 if remainingGas < op.GasCost { return ErrOutOfGas } remainingGas -= op.GasCostContract 结构体中的 Gastype Contract struct { Gas uint64 // 剩余 Gas Code []byte // 合约字节码 Scope *ScopeContext // 当前执行上下文 // ... 其他字段 }Gas 字段:在合约调用开始时初始化为 GasLimit - 已消耗 Gas,逐条指令递减。为什么需要 GAS 指令?应用场景Gas 估算:合约可通过 GAS 指令动态检查剩余 Gas,避免操作中途耗尽。// Solidity 示例:检查剩余 Gas function doSomething() public { uint256 remainingGas = gasleft(); // 编译为 `GAS` 指令 require(remainingGas > 1000, "Insufficient gas"); }优化 Gas 消耗:复杂逻辑中根据剩余 Gas 调整执行路径(如循环次数)。与 gasleft() 的关系Solidity 的 gasleft() 函数会被编译为 GAS 操作码,二者等价。EVM 指令执行的全流程以执行 GAS 指令为例:指令调度:EVM 根据 pc(程序计数器)从合约字节码中读取 0x5A。调用 opGas:通过指令表跳转到对应的处理函数。栈操作:将剩余 Gas 压入栈顶,更新 pc(*pc++)。Gas 扣除:执行前已扣除 GAS 指令的基础成本(2 Gas)。关键设计思想显式 Gas 管理:EVM 要求所有操作明码标价,防止无限循环或资源滥用。栈式虚拟机:通过操作数栈传递参数和结果,GAS 指令符合这一模式。状态隔离:GAS 指令仅读取当前合约的 Gas,不影响其他上下文。扩展:其他 Gas 相关指令指令操作码功能GASPRICE0x3A获取当前交易的 Gas 单价GASLIMIT0x45获取当前区块的 Gas 上限总结opGas 的本质:将当前剩余 Gas 作为 uint256 压入栈顶,无副作用。EVM 的 Gas 机制:每条指令执行前扣除 Gas,不足则回滚。实用意义:智能合约可通过 gasleft() 动态优化执行逻辑。
2025年05月24日
3 阅读
0 评论
1 点赞
2025-05-22
Solidity Style Guide
规范文档https://docs.soliditylang.org/en/v0.8.17/style-guide.html1.缩进每个缩进使用4个空格 应该避免使用空格和tab混合缩进2.间隔对于顶级声明来说应该使用两个空行Yes:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; contract A { // ... } contract B { // ... } contract C { // ... }No:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; contract A { // ... } contract B { // ... } contract C { // ... }合约中的函数间隔使用一个空行 抽象合约中的函数之间不使用空行Yes:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; abstract contract A { function spam() public virtual pure; function ham() public virtual pure; } contract B is A { function spam() public pure override { // ... } function ham() public pure override { // ... } }No:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.6.0 <0.9.0; abstract contract A { function spam() virtual pure public; function ham() public virtual pure; } contract B is A { function spam() public pure override { // ... } function ham() public pure override { // ... } }3.每行最大的长度推荐最大的长度为120个字符对于换行应该遵循的规则:第一个参数不应追加到左括号之后。只使用一个缩进。每个参数应独占一行。结束符); 应单独放在最后一行。对于函数调用的示例Yes:thisFunctionCallIsReallyLong( longArgument1, longArgument2, longArgument3 );No:thisFunctionCallIsReallyLong(longArgument1, longArgument2, longArgument3 ); thisFunctionCallIsReallyLong(longArgument1, longArgument2, longArgument3 ); thisFunctionCallIsReallyLong( longArgument1, longArgument2, longArgument3 ); thisFunctionCallIsReallyLong( longArgument1, longArgument2, longArgument3 ); thisFunctionCallIsReallyLong( longArgument1, longArgument2, longArgument3);赋值语句示例Yes:thisIsALongNestedMapping[being][set][toSomeValue] = someFunction( argument1, argument2, argument3, argument4 );No:thisIsALongNestedMapping[being][set][toSomeValue] = someFunction(argument1, argument2, argument3, argument4);事件定义和事件触发示例Yes:event LongAndLotsOfArgs( address sender, address recipient, uint256 publicKey, uint256 amount, bytes32[] options ); LongAndLotsOfArgs( sender, recipient, publicKey, amount, options );No:event LongAndLotsOfArgs(address sender, address recipient, uint256 publicKey, uint256 amount, bytes32[] options); LongAndLotsOfArgs(sender, recipient, publicKey, amount, options);4.源代码文件编码最好使用 UTF-8 或 ASCII 编码5.Importsimports语句应该始终放在文件的顶部Yes:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; import "./Owned.sol"; contract A { // ... } contract B is Owned { // ... }No:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; contract A { // ... } import "./Owned.sol"; contract B is Owned { // ... }6.函数的放置顺序函数从前往后放置顺序constructorreceive function (if exists)fallback function (if exists)externalpublicinternalprivate对于同一优先级的函数, view and pure放在后面Yes:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract A { constructor() { // ... } receive() external payable { // ... } fallback() external { // ... } // External functions // ... // External functions that are view // ... // External functions that are pure // ... // Public functions // ... // Internal functions // ... // Private functions // ... }No:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract A { // External functions // ... fallback() external { // ... } receive() external payable { // ... } // Private functions // ... // Public functions // ... constructor() { // ... } // Internal functions // ... }7.表达式中的空格应该避免使用多余的空格 括号、方括号或花括号内,单行函数声明除外。Yes:spam(ham[1], Coin({name: "ham"}));No:spam( ham[ 1 ], Coin( { name: "ham" } ) );单行函数声明除外:function singleLine() public { spam(); }逗号和分号之前去除多余的空格Yes:function spam(uint i, Coin coin) public;No:function spam(uint i , Coin coin) public ;赋值和运算符周围的超过一个空格的也需要去除Yes:x = 1; y = 2; longVariable = 3;No:x = 1; y = 2; longVariable = 3;不要在receive和fallback函数名的地方增加多余的空格Yes:receive() external payable { ... } fallback() external { ... }No:receive () external payable { ... } fallback () external { ... }8.控制结构合约、库、函数和结构体主体应该和左括号在一行Yes:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; contract Coin { struct Bank { address owner; uint balance; } }No:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; contract Coin { struct Bank { address owner; uint balance; } }if、while 和 for 关键词与左括号之间应该有一个空格,右括号和左花括号之间也应该有一个空格Yes:if (...) { ... } for (...) { ... }No:if (...) { ... } while(...){ } for (...) { ...;}控制结构中如果包含的语句只在一行时可以省略括号Yes:if (x < 10) x += 1;No:if (x < 10) someArray.push(Coin({ name: 'spam', value: 42 }));对于包含 else 或 else if 子句的 if 块,else 应该与 if 的右括号放在同一行Yes:if (x < 3) { x += 1; } else if (x > 7) { x -= 1; } else { x = 5; } if (x < 3) x += 1; else x -= 1;No:if (x < 3) { x += 1; } else { x -= 1; }9.函数定义对于比较短的函数定义,建议将函数体的左括号与函数定义保持在同一行。右括号的缩进级别应与函数定义相同。左括号前应有一个空格。Yes:function increment(uint x) public pure returns (uint) { return x + 1; } function increment(uint x) public pure onlyOwner returns (uint) { return x + 1; }No:function increment(uint x) public pure returns (uint) { return x + 1; } function increment(uint x) public pure returns (uint){ return x + 1; } function increment(uint x) public pure returns (uint) { return x + 1; } function increment(uint x) public pure returns (uint) { return x + 1;}函数的修饰符应该是按照如下顺序:VisibilityMutabilityVirtualOverride自定义 修饰符Yes:function balance(uint from) public view override returns (uint) { return balanceOf[from]; } function shutdown() public onlyOwner { selfdestruct(owner); }No:function balance(uint from) public override view returns (uint) { return balanceOf[from]; } function shutdown() onlyOwner public { selfdestruct(owner); }对于较长的函数定义,建议将每个参数单独放在一行,并与函数内语句保持相同的缩进级别。右括号和右括号也应单独放在一行,并与函数定义保持相同的缩进级别。Yes:function thisFunctionHasLotsOfArguments( address a, address b, address c, address d, address e, address f ) public { doSomething(); }No:function thisFunctionHasLotsOfArguments(address a, address b, address c, address d, address e, address f) public { doSomething(); } function thisFunctionHasLotsOfArguments(address a, address b, address c, address d, address e, address f) public { doSomething(); } function thisFunctionHasLotsOfArguments( address a, address b, address c, address d, address e, address f) public { doSomething(); }如果函数有修饰符 修饰符放在一列Yes:function thisFunctionNameIsReallyLong(address x, address y, address z) public onlyOwner priced returns (address) { doSomething(); } function thisFunctionNameIsReallyLong( address x, address y, address z ) public onlyOwner priced returns (address) { doSomething(); }No:function thisFunctionNameIsReallyLong(address x, address y, address z) public onlyOwner priced returns (address) { doSomething(); } function thisFunctionNameIsReallyLong(address x, address y, address z) public onlyOwner priced returns (address) { doSomething(); } function thisFunctionNameIsReallyLong(address x, address y, address z) public onlyOwner priced returns (address) { doSomething(); }多行输出参数和返回语句应遵循“最大行长度”部分中建议的换行长行的相同样式Yes:function thisFunctionNameIsReallyLong( address a, address b, address c ) public returns ( address someAddressName, uint256 LongArgument, uint256 Argument ) { doSomething() return ( veryLongReturnArg1, veryLongReturnArg2, veryLongReturnArg3 ); }No:function thisFunctionNameIsReallyLong( address a, address b, address c ) public returns (address someAddressName, uint256 LongArgument, uint256 Argument) { doSomething() return (veryLongReturnArg1, veryLongReturnArg1, veryLongReturnArg1); }对于继承的合约上的base需要参数的构造函数,如果函数很长或难以阅读,建议将base构造函数以与修饰符相同的方式放到多行。Yes:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; // Base contracts just to make this compile contract B { constructor(uint) { } } contract C { constructor(uint, uint) { } } contract D { constructor(uint) { } } contract A is B, C, D { uint x; constructor(uint param1, uint param2, uint param3, uint param4, uint param5) B(param1) C(param2, param3) D(param4) { // do something with param5 x = param5; } }No:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; // Base contracts just to make this compile contract B { constructor(uint) { } } contract C { constructor(uint, uint) { } } contract D { constructor(uint) { } } contract A is B, C, D { uint x; constructor(uint param1, uint param2, uint param3, uint param4, uint param5) B(param1) C(param2, param3) D(param4) { x = param5; } } contract X is B, C, D { uint x; constructor(uint param1, uint param2, uint param3, uint param4, uint param5) B(param1) C(param2, param3) D(param4) { x = param5; } }对于简单的函数定义使用一行function shortFunction() public { doSomething(); }10.Mappings在变量声明中,不要用空格分隔关键字 mapping 和其类型。不要用空格分隔任何嵌套的 mapping 关键字和其类型。Yes:mapping(uint => uint) map; mapping(address => bool) registeredAddresses; mapping(uint => mapping(bool => Data[])) public data; mapping(uint => mapping(uint => s)) data;No:mapping (uint => uint) map; mapping( address => bool ) registeredAddresses; mapping (uint => mapping (bool => Data[])) public data; mapping(uint => mapping (uint => s)) data;11.变量定义定义数组的时候不要将类型和[]分开Yes:uint[] x;No:uint [] x;12.Other Recommendationsstring类型应该使用双引号而不是单引号Yes:str = "foo"; str = "Hamlet says, 'To be or not to be...'";No:str = 'bar'; str = '"Be yourself; everyone else is already taken." -Oscar Wilde';运算符周围应该有一个空格Yes:x = 3; x = 100 / 10; x += 3 + 4; x |= y && z;No:x=3; x = 100/10; x += 3+4; x |= y&&z;高优先级的运算符周围可以省略空格Yes:x = 2**3 + 5; x = 2*y + 3*z; x = (a+b) * (a-b);No:x = 2** 3 + 5; x = y+z; x +=1;13.布局顺序合约中的元素布局顺序Pragma statementsImport statementsInterfacesLibrariesContracts在合约中库、接口顺序Type declarationsState variablesEventsModifiersFunctions14.命名规范命名风格为了避免困惑,以下的将指代不同命名风格b (single lowercase letter)B (single uppercase letter)lowercaseUPPERCASEUPPER_CASE_WITH_UNDERSCORESCapitalizedWords (or CapWords)mixedCase (differs from CapitalizedWords by initial lowercase character!)Note在 CapWords 中使用首字母缩写时,请将首字母缩写的所有字母大写。因此,HTTPServerError 优于 HttpServerError。在 mixedCase 中使用首字母缩写时,请将首字母缩写的所有字母大写,但如果它是名称的开头,则保留第一个字母小写。因此,xmlHTTPRequest 优于 XMLHTTPRequest。避免使用l - Lowercase letter elO - Uppercase letter ohI - Uppercase letter eye切勿将这些字符用作单字母变量名。它们通常与数字 1 和 0 难以区分。合约和库名合约和库的名字应该使用类似 CapWords的风格. Examples: SimpleToken, SmartBank, CertificateHashRepository, Player, Congress, Owned.合约和库的名字应该匹配他们的文件名.如果一个合约文件包含了多个合约或者库,那么这个文件名应该以核心的合约命名。如果可以避免尽量不要这样。Yes:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; // Owned.sol contract Owned { address public owner; constructor() { owner = msg.sender; } modifier onlyOwner { require(msg.sender == owner); _; } function transferOwnership(address newOwner) public onlyOwner { owner = newOwner; } }Congress.sol:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.0 <0.9.0; import "./Owned.sol"; contract Congress is Owned, TokenRecipient { //... }No:// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; // owned.sol contract owned { address public owner; constructor() { owner = msg.sender; } modifier onlyOwner { require(msg.sender == owner); _; } function transferOwnership(address newOwner) public onlyOwner { owner = newOwner; } }Congress.sol:// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.7.0; import "./owned.sol"; contract Congress is owned, tokenRecipient { //... }结构体名字结构体名字应该使用 CapWords 风格. Examples: MyCoin, Position, PositionXY.事件名字事件名字应该使用CapWords 风格. Examples: Deposit, Transfer, Approval, BeforeTransfer, AfterTransfer.函数名字函数名字应该使用混合大小写. Examples: getBalance, transfer, verifyOwner, addMember, changeOwner.函数参数名函数参数应该使用缓和大小写.Examples: initialSupply, account, recipientAddress, senderAddress, newOwner.当编写对自定义结构进行操作的库函数时,该结构应该是第一个参数,并且应始终命名为“self”本地状态变量名使用大小写混合Examples: totalSupply, remainingSupply, balancesOf, creatorAddress, isPreSale, tokenExchangeRate.常量常量应该使用大写字母并且下划线分割 Examples: MAX_BLOCKS, TOKEN_NAME, TOKEN_TICKER, CONTRACT_VERSION.修饰符名使用大小写混合 Examples: onlyBy, onlyAfter, onlyDuringThePreSale.枚举类型名名字命名应该是使用CapWords风格. Examples: TokenGroup, Frame, HashStyle, CharacterLocation.15.NatSpec建议使用 NatSpec 对所有公共接口(ABI 中的所有内容)进行完整注释。// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.16 <0.9.0; /// @author The Solidity Team /// @title A simple storage example contract SimpleStorage { uint storedData; /// Store `x`. /// @param x the new value to store /// @dev stores the number in the state variable `storedData` function set(uint x) public { storedData = x; } /// Return the stored value. /// @dev retrieves the value of the state variable `storedData` /// @return the stored value function get() public view returns (uint) { return storedData; } }Tag Context@titleA title that should describe the contract/interfacecontract, library, interface@authorThe name of the authorcontract, library, interface@noticeExplain to an end user what this doescontract, library, interface, function, public state variable, event@devExplain to a developer any extra detailscontract, library, interface, function, state variable, event@paramDocuments a parameter just like in Doxygen (must be followed by parameter name)function, event@returnDocuments the return variables of a contract’s functionfunction, public state variable@inheritdocCopies all missing tags from the base function (must be followed by the contract name)function, public state variable@custom:...Custom tag, semantics is application-definedeverywhere
2025年05月22日
4 阅读
0 评论
1 点赞
2025-05-16
Uniswap v2
{collapse}{collapse-item label="一、背景" open}传统的中心化交易所买家和卖家进行订单匹配,去中心的交易所引入开创了一种全新的交易方式,通过一种预设的数学公式(比如,常数乘积公式)创建一个流动性池,使得用户可以随时进行交易。自动做市商(Automated Market Maker,简称 AMM)是一种算法,或者说是一种在区块链上运行的智能合约,它允许数字资产之间的去中心化交易。{/collapse-item}{collapse-item label="二、常见的AMM算法" open} ## 1. 恒定乘积做市商(Constant Product Market Maker, CPMM)公式x * y = kx:代币A的数量y:代币B的数量k:恒定乘积(流动性池的总价值)特点代表协议:Uniswap V1/V2滑点机制:交易量越大,价格偏离越多(非线性)。流动性分布:全价格区间均匀分布,资本效率低。2. 恒定和做市商(Constant Sum Market Maker, CSMM)公式x + y = k特点零滑点:价格固定为 1:1(如稳定币交易)。缺点:流动性易耗尽(套利者会抽干某一代币)。代表协议:早期稳定币兑换池(已淘汰)。示例池中有 100 DAI + 100 USDC,用户用 10 DAI 换 10 USDC,池变为 110 DAI + 90 USDC。3. 恒定均值做市商(Constant Mean Market Maker, CMMM)公式x^a y^b z^c = ka,b,c:代币权重(可自定义)。特点支持多代币池(如Balancer的3种代币池)。灵活权重:例如 80% ETH + 20% BTC 的指数基金池。代表协议:Balancer。4. 集中流动性AMM(Concentrated Liquidity)代表协议:Uniswap V3原理LP可自定义价格区间(如 ETH/USDC 在 $1800-$2200 提供流动性)。公式分段处理:在区间内:使用 CPMM(x * y = k)。在区间外:流动性失效(变为单一资产)。优势资本效率提升:最高达4000倍(相比V2)。支持限价单:通过流动性区间模拟。5. 动态费用AMM(Dynamic Fees)代表协议:Trader Joe(Liquidity Book)特点根据市场波动动态调整手续费(如高波动时提高费率)。分档流动性:将价格划分为多个“档位”,每档独立计算流动性。AMM算法的核心问题与解决方案问题解决方案协议示例资本效率低集中流动性(Uniswap V3)Uniswap V3无常损失动态费率或对冲策略Bancor V3多代币池支持恒定均值算法(Balancer)Balancer总结CPMM(Uniswap):简单通用,适合大部分代币对。CSMM:仅适合稳定币,已淘汰。CMMM(Balancer):灵活支持多代币和自定义权重。集中流动性(Uniswap V3):提升资本效率,适合专业做市。{/collapse-item}{collapse-item label="三、实现一个简单的swap" open} SimpleSwap 继承了 ERC20 代币标准,方便记录流动性提供者提供的流动性。在构造器中,我们指定一对代币地址 token0 和 token1,交易所仅支持这对代币。reserve0 和 reserve1 记录了合约中代币的储备量。contract SimpleSwap is ERC20 { // 代币合约 IERC20 public token0; IERC20 public token1; // 代币储备量 uint public reserve0; uint public reserve1; // 构造器,初始化代币地址 constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") { token0 = _token0; token1 = _token1; } }交易所主要有两类参与者:流动性提供者(Liquidity Provider,LP)和交易者(Trader)。下面我们分别实现这两部分的功能。流动性提供流动性提供者给市场提供流动性,让交易者获得更好的报价和流动性,并收取一定费用。首先,我们需要实现添加流动性的功能。当用户向代币池添加流动性时,合约要记录添加的LP份额。这个LP份额主要是用户提供流动性的凭证,交易费的一定比例按 LP 份额分配给流动性提供者,同时也可以赎回代币,销毁 LP 代币,按LP比例取回 tokenA 和 tokenB,具体的tokenA和tokenB数量取决于他们现有数量然后乘LP比例。根据 Uniswap V2,LP份额如下计算:Δx 和 Δy 分别表示一笔交易中token和美元的变化量代币池被首次添加流动性时,LP份额 ΔL 由添加代币数量乘积的平方根决定:非首次添加流动性时,LP份额由添加代币数量占池子代币储备量的比例决定(两个代币的比例取更小的那个):因为 SimpleSwap 合约继承了 ERC20 代币标准,在计算好LP份额后,可以将份额以代币形式铸造LP代币给用户。下面的 addLiquidity() 函数实现了添加流动性的功能,主要步骤如下:将用户添加的代币转入合约,需要用户事先给合约授权。根据公式计算添加的流动性份额,并检查铸造的LP数量。更新合约的代币储备量。给流动性提供者铸造LP代币。释放 Mint 事件。event Mint(address indexed sender, uint amount0, uint amount1); // 添加流动性,转进代币,铸造LP // @param amount0Desired 添加的token0数量 // @param amount1Desired 添加的token1数量 function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){ // 将添加的流动性转入Swap合约,需事先给Swap合约授权 token0.transferFrom(msg.sender, address(this), amount0Desired); token1.transferFrom(msg.sender, address(this), amount1Desired); // 计算添加的流动性 uint _totalSupply = totalSupply(); if (_totalSupply == 0) { // 如果是第一次添加流动性,铸造 L = sqrt(x * y) 单位的LP(流动性提供者)代币 liquidity = sqrt(amount0Desired * amount1Desired); } else { // 如果不是第一次添加流动性,按添加代币的数量比例铸造LP,取两个代币更小的那个比例 liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1); } // 检查铸造的LP数量 require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED'); // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); // 给流动性提供者铸造liquidity个LP代币,代表他们提供的流动性 _mint(msg.sender, liquidity); emit Mint(msg.sender, amount0Desired, amount1Desired); }接下来,我们需要实现移除流动性的功能。当用户从池子中移除流动性 ΔL 时,合约要销毁LP份额代币,并按比例将代币返还给用户。返还代币的计算公式如下:下面的 removeLiquidity() 函数实现移除流动性的功能,主要步骤如下:获取合约中的代币余额。按LP的比例计算要转出的代币数量。检查代币数量。销毁LP份额。将相应的代币转账给用户。更新储备量。释放 Burn 事件。// 移除流动性,销毁LP,转出代币 // 转出数量 = (liquidity / totalSupply_LP) * reserve // @param liquidity 移除的流动性数量 function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) { // 获取余额 uint balance0 = token0.balanceOf(address(this)); uint balance1 = token1.balanceOf(address(this)); // 按LP的比例计算要转出的代币数量 uint _totalSupply = totalSupply(); amount0 = liquidity * balance0 / _totalSupply; amount1 = liquidity * balance1 / _totalSupply; // 检查代币数量 require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED'); // 销毁LP _burn(msg.sender, liquidity); // 转出代币 token0.transfer(msg.sender, amount0); token1.transfer(msg.sender, amount1); // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); emit Burn(msg.sender, amount0, amount1); }至此,合约中与流动性提供者相关的功能完成了,接下来是交易的部分。交易在Swap合约中,用户可以使用一种代币交易另一种。那么我用 Δx单位的 token0,可以交换多少单位的 token1 呢?下面我们来简单推导一下。根据恒定乘积公式,交易前:交易后,有:交易前后 k 值不变,联立上面等式,可以得到:因此,可以交换到的代币数量 Δy 由 Δx,x,和 y 决定。注意 Δx 和 Δy 的符号相反,因为转入会增加代币储备量,而转出会减少。下面的 getAmountOut() 实现了给定一个资产的数量和代币对的储备,计算交换另一个代币的数量。// 给定一个资产的数量和代币对的储备,计算交换另一个代币的数量 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) { require(amountIn > 0, 'INSUFFICIENT_AMOUNT'); require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY'); amountOut = amountIn * reserveOut / (reserveIn + amountIn); }有了这一核心公式后,我们可以着手实现交易功能了。下面的 swap() 函数实现了交易代币的功能,主要步骤如下:用户在调用函数时指定用于交换的代币数量,交换的代币地址,以及换出另一种代币的最低数量。判断是 token0 交换 token1,还是 token1 交换 token0。利用上面的公式,计算交换出代币的数量。判断交换出的代币是否达到了用户指定的最低数量,这里类似于交易的滑点。将用户的代币转入合约。将交换的代币从合约转给用户。更新合约的代币储备量。释放 Swap 事件。// swap代币 // @param amountIn 用于交换的代币数量 // @param tokenIn 用于交换的代币合约地址 // @param amountOutMin 交换出另一种代币的最低数量 function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){ require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT'); require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN'); uint balance0 = token0.balanceOf(address(this)); uint balance1 = token1.balanceOf(address(this)); if(tokenIn == token0){ // 如果是token0交换token1 tokenOut = token1; // 计算能交换出的token1数量 amountOut = getAmountOut(amountIn, balance0, balance1); require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); // 进行交换 tokenIn.transferFrom(msg.sender, address(this), amountIn); tokenOut.transfer(msg.sender, amountOut); }else{ // 如果是token1交换token0 tokenOut = token0; // 计算能交换出的token1数量 amountOut = getAmountOut(amountIn, balance1, balance0); require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); // 进行交换 tokenIn.transferFrom(msg.sender, address(this), amountIn); tokenOut.transfer(msg.sender, amountOut); } // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut)); }Swap 合约SimpleSwap 的完整代码如下:// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract SimpleSwap is ERC20 { // 代币合约 IERC20 public token0; IERC20 public token1; // 代币储备量 uint public reserve0; uint public reserve1; // 事件 event Mint(address indexed sender, uint amount0, uint amount1); event Burn(address indexed sender, uint amount0, uint amount1); event Swap( address indexed sender, uint amountIn, address tokenIn, uint amountOut, address tokenOut ); // 构造器,初始化代币地址 constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") { token0 = _token0; token1 = _token1; } // 取两个数的最小值 function min(uint x, uint y) internal pure returns (uint z) { z = x < y ? x : y; } // 计算平方根 babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) 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; } } // 添加流动性,转进代币,铸造LP // 如果首次添加,铸造的LP数量 = sqrt(amount0 * amount1) // 如果非首次,铸造的LP数量 = min(amount0/reserve0, amount1/reserve1)* totalSupply_LP // @param amount0Desired 添加的token0数量 // @param amount1Desired 添加的token1数量 function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){ // 将添加的流动性转入Swap合约,需事先给Swap合约授权 token0.transferFrom(msg.sender, address(this), amount0Desired); token1.transferFrom(msg.sender, address(this), amount1Desired); // 计算添加的流动性 uint _totalSupply = totalSupply(); if (_totalSupply == 0) { // 如果是第一次添加流动性,铸造 L = sqrt(x * y) 单位的LP(流动性提供者)代币 liquidity = sqrt(amount0Desired * amount1Desired); } else { // 如果不是第一次添加流动性,按添加代币的数量比例铸造LP,取两个代币更小的那个比例 liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1); } // 检查铸造的LP数量 require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED'); // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); // 给流动性提供者铸造LP代币,代表他们提供的流动性 _mint(msg.sender, liquidity); emit Mint(msg.sender, amount0Desired, amount1Desired); } // 移除流动性,销毁LP,转出代币 // 转出数量 = (liquidity / totalSupply_LP) * reserve // @param liquidity 移除的流动性数量 function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) { // 获取余额 uint balance0 = token0.balanceOf(address(this)); uint balance1 = token1.balanceOf(address(this)); // 按LP的比例计算要转出的代币数量 uint _totalSupply = totalSupply(); amount0 = liquidity * balance0 / _totalSupply; amount1 = liquidity * balance1 / _totalSupply; // 检查代币数量 require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED'); // 销毁LP _burn(msg.sender, liquidity); // 转出代币 token0.transfer(msg.sender, amount0); token1.transfer(msg.sender, amount1); // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); emit Burn(msg.sender, amount0, amount1); } // 给定一个资产的数量和代币对的储备,计算交换另一个代币的数量 // 由于乘积恒定 // 交换前: k = x * y // 交换后: k = (x + delta_x) * (y + delta_y) // 可得 delta_y = - delta_x * y / (x + delta_x) // 正/负号代表转入/转出 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) { require(amountIn > 0, 'INSUFFICIENT_AMOUNT'); require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY'); amountOut = amountIn * reserveOut / (reserveIn + amountIn); } // swap代币 // @param amountIn 用于交换的代币数量 // @param tokenIn 用于交换的代币合约地址 // @param amountOutMin 交换出另一种代币的最低数量 function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){ require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT'); require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN'); uint balance0 = token0.balanceOf(address(this)); uint balance1 = token1.balanceOf(address(this)); if(tokenIn == token0){ // 如果是token0交换token1 tokenOut = token1; // 计算能交换出的token1数量 amountOut = getAmountOut(amountIn, balance0, balance1); require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); // 进行交换 tokenIn.transferFrom(msg.sender, address(this), amountIn); tokenOut.transfer(msg.sender, amountOut); }else{ // 如果是token1交换token0 tokenOut = token0; // 计算能交换出的token1数量 amountOut = getAmountOut(amountIn, balance1, balance0); require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); // 进行交换 tokenIn.transferFrom(msg.sender, address(this), amountIn); tokenOut.transfer(msg.sender, amountOut); } // 更新储备量 reserve0 = token0.balanceOf(address(this)); reserve1 = token1.balanceOf(address(this)); emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut)); } }1.部署两个ERC20合约token0和token12.部署 SimpleSwap 合约,并将token0和token1的地址作为构造函数参数3.调用两个ERC20代币的approve()函数,分别给 SimpleSwap 合约授权 1000 单位代币。4.调用 SimpleSwap 合约的 addLiquidity() 函数给交易所添加流动性,token0 和 token1 分别添加 100 单位。5.调用 SimpleSwap 合约的 balanceOf() 函数查看用户的LP份额,这里应该为 100。6.调用 SimpleSwap 合约的 swap() 函数进行代币交易,用 100 单位的 token07.调用 SimpleSwap 合约的 reserve0 和 reserve1 函数查看合约中的代币储备粮,应为 200 和 50。上一步我们利用 100 单位的 token0 交换了 50 单位的 token 1{/collapse-item}{/collapse}
2025年05月16日
5 阅读
0 评论
1 点赞
2025-05-14
ERC20 Contract
{collapse}{collapse-item label="一、定义" open}ERC20ERC20是以太坊上的代币标准,来自2015年11月V神参与的EIP20。它实现了代币转账的基本逻辑:账户余额(balanceOf())转账(transfer())授权转账(transferFrom())授权(approve())代币总供给(totalSupply())授权转账额度(allowance())代币信息(可选):名称(name()),代号(symbol()),小数位数(decimals())IERC20IERC20是ERC20代币标准的接口合约,规定了ERC20代币需要实现的函数和事件。 之所以需要定义接口,是因为有了规范后,就存在所有的ERC20代币都通用的函数名称,输入参数,输出参数。 在接口函数中,只需要定义函数名称,输入参数,输出参数,并不关心函数内部如何实现。 由此,函数就分为内部和外部两个内容,一个重点是实现,另一个是对外接口,约定共同数据。 这就是为什么需要ERC20.sol和IERC20.sol两个文件实现一个合约。事件IERC20定义了2个事件:Transfer事件和Approval事件,分别在转账和授权时被释放/** * @dev 释放条件:当 `value` 单位的货币从账户 (`from`) 转账到另一账户 (`to`)时. */ event Transfer(address indexed from, address indexed to, uint256 value); /** * @dev 释放条件:当 `value` 单位的货币从账户 (`owner`) 授权给另一账户 (`spender`)时. */ event Approval(address indexed owner, address indexed spender, uint256 value);函数IERC20定义了6个函数,提供了转移代币的基本功能,并允许代币获得批准,以便其他链上第三方使用。totalSupply()返回代币总供给/** * @dev 返回代币总供给. */ function totalSupply() external view returns (uint256);balanceOf()返回账户余额/** * @dev 返回账户`account`所持有的代币数. */ function balanceOf(address account) external view returns (uint256);transfer()转账/** * @dev 转账 `amount` 单位代币,从调用者账户到另一账户 `to`. * * 如果成功,返回 `true`. * * 释放 {Transfer} 事件. */ function transfer(address to, uint256 amount) external returns (bool);allowance()返回授权额度/** * @dev 返回`owner`账户授权给`spender`账户的额度,默认为0。 * * 当{approve} 或 {transferFrom} 被调用时,`allowance`会改变. */ function allowance(address owner, address spender) external view returns (uint256);approve()授权/** * @dev 调用者账户给`spender`账户授权 `amount`数量代币。 * * 如果成功,返回 `true`. * * 释放 {Approval} 事件. */ function approve(address spender, uint256 amount) external returns (bool);transferFrom()授权转账/** * @dev 通过授权机制,从`from`账户向`to`账户转账`amount`数量代币。转账的部分会从调用者的`allowance`中扣除。 * * 如果成功,返回 `true`. * * 释放 {Transfer} 事件. */ function transferFrom( address from, address to, uint256 amount ) external returns (bool);实现ERC20现在我们写一个ERC20,将IERC20规定的函数简单实现。状态变量我们需要状态变量来记录账户余额,授权额度和代币信息。其中balanceOf, allowance和totalSupply为public类型,会自动生成一个同名getter函数,实现IERC20规定的balanceOf(), allowance()和totalSupply()。而name, symbol, decimals则对应代币的名称,代号和小数位数。注意:用override修饰public变量,会重写继承自父合约的与变量同名的getter函数,比如IERC20中的balanceOf()函数。mapping(address => uint256) public override balanceOf; mapping(address => mapping(address => uint256)) public override allowance; uint256 public override totalSupply; // 代币总供给 string public name; // 名称 string public symbol; // 代号 uint8 public decimals = 18; // 小数位数函数构造函数:初始化代币名称、代号。constructor(string memory name_, string memory symbol_){ name = name_; symbol = symbol_; }transfer()函数:实现IERC20中的transfer函数,代币转账逻辑。调用方扣除amount数量代币,接收方增加相应代币。土狗币会魔改这个函数,加入税收、分红、抽奖等逻辑。function transfer(address recipient, uint amount) public override returns (bool) { balanceOf[msg.sender] -= amount; balanceOf[recipient] += amount; emit Transfer(msg.sender, recipient, amount); return true; }approve()函数:实现IERC20中的approve函数,代币授权逻辑。被授权方spender可以支配授权方的amount数量的代币。spender可以是EOA账户,也可以是合约账户:当你用uniswap交易代币时,你需要将代币授权给uniswap合约。function approve(address spender, uint amount) public override returns (bool) { allowance[msg.sender][spender] = amount; emit Approval(msg.sender, spender, amount); return true; }transferFrom()函数:实现IERC20中的transferFrom函数,授权转账逻辑。被授权方将授权方sender的amount数量的代币转账给接收方recipient。function transferFrom( address sender, address recipient, uint amount ) public override returns (bool) { allowance[sender][msg.sender] -= amount; balanceOf[sender] -= amount; balanceOf[recipient] += amount; emit Transfer(sender, recipient, amount); return true; }mint()函数:铸造代币函数,不在IERC20标准中。这里为了教程方便,任何人可以铸造任意数量的代币,实际应用中会加权限管理,只有owner可以铸造代币:function mint(uint amount) external { balanceOf[msg.sender] += amount; totalSupply += amount; emit Transfer(address(0), msg.sender, amount); }burn()函数:销毁代币函数,不在IERC20标准中。function burn(uint amount) external { balanceOf[msg.sender] -= amount; totalSupply -= amount; emit Transfer(msg.sender, address(0), amount); }{/collapse-item}{collapse-item label="二、应用" open}1. 代币水龙头代币水龙头就是让用户免费领代币的网站/应用。实现一个简版的ERC20水龙头,逻辑非常简单:我们将一些ERC20代币转到水龙头合约里,用户可以通过合约的requestToken()函数来领取100单位的代币,每个地址只能领一次。状态变量我们在水龙头合约中定义3个状态变量amountAllowed设定每次能领取代币数量(默认为100,不是一百枚,因为代币有小数位数)。tokenContract记录发放的ERC20代币合约地址。requestedAddress记录领取过代币的地址。uint256 public amountAllowed = 100; // 每次领 100 单位代币 address public tokenContract; // token合约地址 mapping(address => bool) public requestedAddress; // 记录领取过代币的地址事件水龙头合约中定义了1个SendToken事件,记录了每次领取代币的地址和数量,在requestTokens()函数被调用时释放。// SendToken事件 event SendToken(address indexed Receiver, uint256 indexed Amount); 函数合约中只有两个函数:构造函数:初始化tokenContract状态变量,确定发放的ERC20代币地址。// 部署时设定ERC20代币合约 constructor(address _tokenContract) { tokenContract = _tokenContract; // set token contract }requestTokens()函数,用户调用它可以领取ERC20代币。// 用户领取代币函数 function requestTokens() external { require(!requestedAddress[msg.sender], "Can't Request Multiple Times!"); // 每个地址只能领一次 IERC20 token = IERC20(tokenContract); // 创建IERC20合约对象 require(token.balanceOf(address(this)) >= amountAllowed, "Faucet Empty!"); // 水龙头空了 token.transfer(msg.sender, amountAllowed); // 发送token requestedAddress[msg.sender] = true; // 记录领取地址 emit SendToken(msg.sender, amountAllowed); // 释放SendToken事件 }在Remix上测试(1).先将自定义的dog合约部署查看日志部署成功 生成合约地址(2).使用mint铸造1000个dog到自己钱包中查看日志触发transfer事件铸造成功 从地址0x0转了100个dog到了自己的钱包地址(3).选择水龙头合约部署 填写dog代币合约地址作为水龙头合约构造函数参数查看日志合约部署成功 生成合约地址(4).调用dog合约的transfer发送200个dog给水龙头合约查看日志钱包转了200个dog到水龙头合约地址(5).现在使用一个钱包和水龙头合约进行交互获取dog钱包0x6......7f2收到了100个dog查询dog合约看下钱包0x6......7f2拥有100个dog代币2. Airdrop由于通常接收空投的用户数量较多,项目方逐一发送转账并不实际。通过使用智能合约批量发送ERC20代币,可以显著提高空投的效率。空投代币合约空投合约的逻辑很简单:通过循环,单笔交易将ERC20代币发送到多个地址。合约包含2以下函数:函数getSum():返回数组的总和uint。// sum function for arrays function getSum(uint256[] calldata _arr) public pure returns(uint sum) { for(uint i = 0; i < _arr.length; i++) sum = sum + _arr[i]; }功能multiTransferToken():发送代币空投ERC20,包含3参数:_token:代币合约地址(address类型)_addresses:接收空投的用户地址数组(address[]类型)_amounts_addresses:(uint[]类型)中每个地址对应空投金额数组该函数包含2以下检查:第一个require检查数组的长度是否_addresses等于数组的长度_amounts。第二个require检查空投合约的授权限额是否大于要空投的代币总量。/// @notice Transfer ERC20 tokens to multiple addresses, authorization is required before use /// /// @param _token The address of ERC20 token for transfer /// @param _addresses The array of airdrop addresses /// @param _amounts The array of amount of tokens (airdrop amount for each address) function multiTransferToken( address _token, address[] calldata _addresses, uint256[] calldata _amounts ) external { // Check: The length of _addresses array should be equal to the length of _amounts array require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL"); IERC20 token = IERC20(_token); // Declare IERC contract variable uint _amountSum = getSum(_amounts); // Calculate the total amount of airdropped tokens // Check: The authorized amount of tokens should be greater than or equal to the total amount of airdropped tokens require(token.allowance(msg.sender, address(this)) >= _amountSum, "Need Approve ERC20 token"); // for loop, use transferFrom function to send airdrops for (uint8 i; i < _addresses.length; i++) { token.transferFrom(msg.sender, _addresses[i], _amounts[i]); } }multiTransferETH()功能:发送ETH空投,2参数:_addresses:接收空投的用户地址数组(address[]类型)_amounts:空投金额数组,对应_addresses(uint[]类型)中每个地址的数量/// Transfer ETH to multiple addresses function multiTransferETH( address payable[] calldata _addresses, uint256[] calldata _amounts ) public payable { // Check: _addresses and _amounts arrays should have the same length require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL"); // Calculate total amount of ETH to be airdropped uint _amountSum = getSum(_amounts); // Check: transferred ETH should equal total amount require(msg.value == _amountSum, "Transfer amount error"); // Use a for loop to transfer ETH using transfer function for (uint256 i = 0; i < _addresses.length; i++) { _addresses[i].transfer(_amounts[i]); } }在Remix上测试(1).先将自定义的dog合约部署查看日志部署成功 生成合约地址(2).使用mint铸造1000个dog到自己钱包中查看日志触发transfer事件铸造成功 从地址0x0转了100个dog到了自己的钱包地址(3).选择airdrop合约部署查看日志airdrop合约部署成功 生成合约地址(4).使用dog合约的approve函数授权airdrop合约拥有其中800个dog的权限查看日志触发approve事件 授权成功 授权airdrop合约操作钱包地址的800个dog(5).现在管理员就可以通过和airdrop合约交互 指定要发放空投的钱包地址对应的dog个数查看日志钱包被空投了dog(6)拿其中一个钱包去dog合约地址调用balanceof查询钱包的dog个数是100和空投的数量一致{/collapse-item}{/collapse}
2025年05月14日
4 阅读
1 评论
1 点赞
1
...
5
6