首页
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
页面
搜索到
34
篇与
的结果
2025-08-05
Uniswap v2-Liquidity
一、Create pool1.createPair创建流动性时,需要确保交易对存在,如果不存在将会调用IUniswapV2Factory(factory).createPair创建交易对// Uniswap/v2-periphery/contracts/UniswapV2Router02.sol function _addLiquidity( address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin ) internal virtual returns (uint amountA, uint amountB) { // create the pair if it doesn't exist yet if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) { IUniswapV2Factory(factory).createPair(tokenA, tokenB); } .... }这个函数是 UniswapV2Factory 的核心方法,用于动态创建新的代币交易对合约。// Uniswap/v2-core/contracts/UniswapV2Factory.sol function createPair(address tokenA, address tokenB) external returns (address pair) { require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES'); (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS'); require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient bytes memory bytecode = type(UniswapV2Pair).creationCode; bytes32 salt = keccak256(abi.encodePacked(token0, token1)); assembly { pair := create2(0, add(bytecode, 32), mload(bytecode), salt) } IUniswapV2Pair(pair).initialize(token0, token1); getPair[token0][token1] = pair; getPair[token1][token0] = pair; // populate mapping in the reverse direction allPairs.push(pair); emit PairCreated(token0, token1, pair, allPairs.length); }参数校验require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES'); (address token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS'); require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS');禁止相同代币:防止创建无意义的交易对(如 ETH/ETH)。代币排序:按地址升序排列 token0 和 token1,确保唯一性(避免重复创建 ETH/USDT 和 USDT/ETH)。零地址检查:防止无效代币。重复创建检查:确保该交易对尚未存在。使用 CREATE2 部署合约bytes memory bytecode = type(UniswapV2Pair).creationCode; bytes32 salt = keccak256(abi.encodePacked(token0, token1)); assembly { pair := create2(0, add(bytecode, 32), mload(bytecode), salt) }CREATE2 特性:通过 salt(代币对哈希)确定合约地址,地址可预测且与部署顺序无关。公式:address = hash(0xFF, factory, hash(token0, token1), init_code_hash)。操作步骤:获取 UniswapV2Pair 合约的初始化字节码(creationCode)。用 token0 和 token1 生成唯一的 salt。内联汇编调用 create2,返回新合约地址 pair。当 Uniswap V2 开发时(2020 年),Solidity 尚未内置 new 关键字支持 CREATE2(直到 Solidity 0.8+ 才原生支持)。必须通过汇编直接调用 EVM 的 CREATE2 操作码。参数解释:0:不发送 Ether(value)。add(bytecode, 32):跳过字节码的前 32 字节(长度字段),指向实际代码。mload(bytecode):读取字节码长度。salt:基于代币对的唯一标识(keccak256(token0, token1))。初始化交易对IUniswapV2Pair(pair).initialize(token0, token1);调用新 Pair 合约的 initialize 方法,设置代币地址 token0 和 token1。为什么单独初始化?分离部署和初始化,避免构造函数参数传递复杂性。确保代币顺序与 Factory 一致。更新工厂状态getPair[token0][token1] = pair; getPair[token1][token0] = pair; // 反向映射 allPairs.push(pair); emit PairCreated(token0, token1, pair, allPairs.length);记录交易对:双向映射 getPair,支持通过任意代币顺序查询。allPairs 数组保存所有交易对地址,便于遍历。事件日志:通知监听者新交易对创建。分叉主网测试createPairfunction test_createPair() public { ERC20 token = new ERC20("test", "TEST", 18); address pair = factory.createPair(address(token), address(weth)); address token0 = IUniswapV2Pair(pair).token0(); address token1 = IUniswapV2Pair(pair).token1(); if (address(token) < WETH) { assertEq(token0, address(token), "token 0"); assertEq(token1, WETH, "token 1"); } else { assertEq(token0, WETH, "token 0"); assertEq(token1, address(token), "token 1"); } }二、add liquidity// Uniswap/v2-periphery/contracts/UniswapV2Router02.sol function addLiquidity( address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin, address to, uint deadline ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) { (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin); address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA); TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB); liquidity = IUniswapV2Pair(pair).mint(to); }该函数允许用户向 Uniswap V2 交易对(如 ETH/USDT)添加流动性,并获取对应的 LP 代币(流动性凭证)。主要步骤包括:计算最优添加量、代币转账、铸造 LP 代币。参数说明参数类型作用tokenAaddress代币 A 的地址tokenBaddress代币 B 的地址amountADesireduint用户希望添加的 A 代币数量amountBDesireduint用户希望添加的 B 代币数量amountAMinuint用户可接受的最少 A 代币实际添加量(防滑点)amountBMinuint用户可接受的最少 B 代币实际添加量(防滑点)toaddress接收 LP 代币的地址deadlineuint交易过期时间(Unix 时间戳)代码逻辑分步解析1. 修饰符 ensure(deadline)modifier ensure(uint deadline) { require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED'); _; }作用:确保交易在 deadline 之前被执行,否则回滚(防止过期交易被意外打包)。2. 计算实际添加量 _addLiquidity(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);内部函数 _addLiquidity 会:检查交易对是否存在(若不存在则自动创建)。根据当前池子比例计算最优的 amountA 和 amountB(避免大幅改变价格)。验证 amountA ≥ amountAMin 和 amountB ≥ amountBMin(防止高滑点损失)。3. 获取交易对地址address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);pairFor:通过工厂合约和代币地址计算 Pair 合约地址(使用 CREATE2 确定性地址)。例如:tokenA = WETH, tokenB = USDT → 返回 WETH/USDT 交易对地址。4. 代币转账到交易对TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA); TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);safeTransferFrom:安全地将代币从用户钱包转入交易对合约。需要用户提前授权(approve)给 Router 合约。若转账失败(如余额不足),会回滚交易。5. 铸造 LP 代币liquidity = IUniswapV2Pair(pair).mint(to);mint:调用 Pair 合约的铸造函数:根据添加的流动性比例,计算应铸造的 LP 代币数量。将 LP 代币发送给 to 地址。返回铸造的 LP 数量(liquidity)。// Uniswap/v2-periphery/contracts/UniswapV2Router02.sol function _addLiquidity( address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin ) internal virtual returns (uint amountA, uint amountB) { // create the pair if it doesn't exist yet if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) { IUniswapV2Factory(factory).createPair(tokenA, tokenB); } (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB); if (reserveA == 0 && reserveB == 0) { (amountA, amountB) = (amountADesired, amountBDesired); } else { uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB); if (amountBOptimal <= amountBDesired) { require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); (amountA, amountB) = (amountADesired, amountBOptimal); } else { uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA); assert(amountAOptimal <= amountADesired); require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); (amountA, amountB) = (amountAOptimal, amountBDesired); } } }该函数是 Uniswap V2 添加流动性的核心内部逻辑,负责:自动创建交易对(如果不存在)。按最优比例计算实际添加量(防止改变市场价格)。验证滑点限制(amountAMin 和 amountBMin)。代码逻辑分步解析1. 创建交易对(如果不存在)if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) { IUniswapV2Factory(factory).createPair(tokenA, tokenB); }作用:检查代币对是否存在,若不存在则通过工厂合约创建。关键点:使用 CREATE2 确定性地址,确保同一代币对在不同网络的地址一致。新创建的池子初始储备金为 0。2. 获取当前储备金(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);getReserves:从 Pair 合约中读取 reserve0 和 reserve1(已按代币排序)。3. 处理空池情况if (reserveA == 0 && reserveB == 0) { (amountA, amountB) = (amountADesired, amountBDesired); }逻辑:若池子为空,直接接受用户提供的全部代币量(成为初始流动性提供者)。示例:用户首次添加 1 ETH + 2000 USDT → 池子比例设为 1:2000。4. 非空池的比例计算uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB); if (amountBOptimal <= amountBDesired) { require(amountBOptimal >= amountBMin, 'INSUFFICIENT_B_AMOUNT'); (amountA, amountB) = (amountADesired, amountBOptimal); } else { uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA); assert(amountAOptimal <= amountADesired); require(amountAOptimal >= amountAMin, 'INSUFFICIENT_A_AMOUNT'); (amountA, amountB) = (amountAOptimal, amountBDesired); }(1) 计算最优比例quote 函数:根据当前池子比例计算匹配量。// 公式:amountBOptimal = (amountADesired * reserveB) / reserveA两种场景:Case 1:用户提供的 amountBDesired 足够匹配 amountADesired(amountBOptimal ≤ amountBDesired)→ 使用 amountADesired 和 amountBOptimal。Case 2:用户提供的 amountBDesired 不足 → 反向计算 amountAOptimal。(2) 滑点验证require(amountBOptimal >= amountBMin):确保实际添加的 amountB 不低于用户设置的最小值(防止高滑点损失)。assert(amountAOptimal <= amountADesired):内部安全检查(应恒成立,否则代码有误)。// Uniswap/v2-core/contracts/UniswapV2Pair.sol function mint(address to) external lock returns (uint liquidity) { (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings uint balance0 = IERC20(token0).balanceOf(address(this)); uint balance1 = IERC20(token1).balanceOf(address(this)); uint amount0 = balance0.sub(_reserve0); uint amount1 = balance1.sub(_reserve1); bool feeOn = _mintFee(_reserve0, _reserve1); uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee if (_totalSupply == 0) { liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY); _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens } else { liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1); } require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED'); _mint(to, liquidity); _update(balance0, balance1, _reserve0, _reserve1); if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date emit Mint(msg.sender, amount0, amount1); }mint 是 UniswapV2 Pair 合约的核心函数,负责向流动性池添加流动性并铸造对应的 LP 代币。调用者为 Router 合约,用户通过 Router 间接调用。关键步骤解析1. 获取储备金和当前余额(uint112 _reserve0, uint112 _reserve1,) = getReserves(); uint balance0 = IERC20(token0).balanceOf(address(this)); uint balance1 = IERC20(token1).balanceOf(address(this)); uint amount0 = balance0.sub(_reserve0); // 新增的 token0 数量 uint amount1 = balance1.sub(_reserve1); // 新增的 token1 数量作用:计算用户实际转入的代币量(amount0 和 amount1)。注意:getReserves() 返回的是上一次调用 _update 时的值,而 balanceOf 是实时余额。2. 手续费处理(_mintFee)bool feeOn = _mintFee(_reserve0, _reserve1);逻辑:如果协议手续费开启(默认 0.05% 给工厂合约),检查是否需要铸造手续费对应的 LP 代币。手续费计算基于 kLast(上次手续费结算时的 reserve0 * reserve1)与当前储备金的差值。目的:确保流动性提供者(LP)在提取流动性时支付应得的手续费。3. 计算应铸造的 LP 数量情况1:首次添加流动性(_totalSupply == 0)liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY); _mint(address(0), MINIMUM_LIQUIDITY); // 永久锁定初始流动性MINIMUM_LIQUIDITY(默认 1000 wei)被永久锁定,防止首次添加时 LP 代币被恶意操纵。初始流动性决定了 LP 代币的总供应基准。情况2:非首次添加liquidity = Math.min( amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1 );按代币添加量的较小比例铸造 LP,确保新增流动性不改变当前价格比例。4. 校验与铸造 LP 代币require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED'); _mint(to, liquidity); // 给用户地址铸造 LP 代币防呆检查:防止零流动性铸造。ERC20 操作:通过 _mint 将 LP 代币发送给用户。5. 更新储备金和触发事件_update(balance0, balance1, _reserve0, _reserve1); if (feeOn) kLast = uint(reserve0).mul(reserve1); emit Mint(msg.sender, amount0, amount1);_update:更新储备金为最新余额,并计算累计价格。kLast:记录当前储备金乘积,用于下次手续费计算。事件:通知外部流动性添加详情。分叉主网测试addLiquiditycontract UniswapV2LiquidityTest is Test { IWETH private constant weth = IWETH(WETH); IERC20 private constant dai = IERC20(DAI); IUniswapV2Router02 private constant router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02); IUniswapV2Pair private constant pair = IUniswapV2Pair(UNISWAP_V2_PAIR_DAI_WETH); address private constant user = address(100); function setUp() public { // Fund WETH to user deal(user, 100 * 1e18); vm.startPrank(user); weth.deposit{value: 100 * 1e18}(); weth.approve(address(router), type(uint256).max); vm.stopPrank(); // Fund DAI to user deal(DAI, user, 1000000 * 1e18); vm.startPrank(user); dai.approve(address(router), type(uint256).max); vm.stopPrank(); } function test_addLiquidity() public { vm.prank(user); (,, uint256 liquidity) = router.addLiquidity(WETH, DAI, 100 * 1e18, 1000000 * 1e18, 1, 1, user, block.timestamp); console2.log("LP:", liquidity); assertGt(pair.balanceOf(user), 0, "LP = 0"); assertEq(pair.balanceOf(user), liquidity, "user LP = liquidity"); } }三、Remove liquidity// Uniswap/v2-periphery/contracts/UniswapV2Router02.sol function removeLiquidity( address tokenA, address tokenB, uint liquidity, uint amountAMin, uint amountBMin, address to, uint deadline ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) { address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to); (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB); (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); }该函数是 UniswapV2 Router 合约的核心方法,允许用户销毁 LP 代币并提取对应比例的两种底层代币。主要步骤包括:转移 LP 代币、销毁 LP 计算赎回量、滑点验证和代币转账。参数说明参数类型作用tokenA, tokenBaddress流动池中的两种代币地址liquidityuint要销毁的 LP 代币数量amountAMin, amountBMinuint用户能接受的最少提取量(滑点保护)toaddress接收提取代币的地址deadlineuint交易过期时间执行流程分步解析1. 权限与有效期检查ensure(deadline) // 修饰器检查交易未过期防止用户签名交易被延迟执行后因价格波动造成损失。2. 获取交易对地址address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);通过工厂合约和代币地址计算 Pair 合约的确定性地址(CREATE2 生成)。3. 转移 LP 代币到交易对IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity);前置条件:用户需提前 approve Router 合约操作其 LP 代币。将 LP 代币从用户转到 Pair 合约,准备销毁。4. 销毁 LP 并计算赎回量(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);burn 函数内部逻辑:根据 LP 占总供应量的比例,计算应返还的代币量:将 amount0 和 amount1 转给 to 地址。销毁 LP 代币,更新储备金。5. 代币排序与金额映射(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB); (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);将 Pair 合约返回的 amount0/amount1 按用户输入的 tokenA/tokenB 顺序重新映射,确保接口友好性。6. 滑点验证require(amountA >= amountAMin, 'INSUFFICIENT_A_AMOUNT'); require(amountB >= amountBMin, 'INSUFFICIENT_B_AMOUNT');防止因价格波动或抢跑攻击导致用户提取量过少。例如:用户设置 amountAMin = 0.9 ETH,若实际仅提取 0.8 ETH,交易回滚。// Uniswap/v2-core/contracts/UniswapV2Pair.sol function burn(address to) external lock returns (uint amount0, uint amount1) { (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings address _token0 = token0; // gas savings address _token1 = token1; // gas savings uint balance0 = IERC20(_token0).balanceOf(address(this)); uint balance1 = IERC20(_token1).balanceOf(address(this)); uint liquidity = balanceOf[address(this)]; bool feeOn = _mintFee(_reserve0, _reserve1); uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED'); _burn(address(this), liquidity); _safeTransfer(_token0, to, amount0); _safeTransfer(_token1, to, amount1); balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); _update(balance0, balance1, _reserve0, _reserve1); if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date emit Burn(msg.sender, amount0, amount1, to); }burn 是 UniswapV2 Pair 合约的关键函数,负责销毁 LP 代币并返还对应比例的底层代币。它是流动性移除的底层实现,由 Router 合约调用。执行流程与关键机制1. 状态读取与缓存(Gas 优化)(uint112 _reserve0, uint112 _reserve1,) = getReserves(); address _token0 = token0; // 代币0地址缓存 address _token1 = token1; // 代币1地址缓存 uint balance0 = IERC20(_token0).balanceOf(address(this)); uint balance1 = IERC20(_token1).balanceOf(address(this)); uint liquidity = balanceOf[address(this)]; // 待销毁的LP数量(用户转到 pair 合约)目的:减少链上读取次数(_reserve0、_token0等只读一次),节省 Gas。注意:balance0 和 balance1 是当前合约的实际余额,可能包含未计入储备金的代币(如手续费)。2. 手续费处理(_mintFee)bool feeOn = _mintFee(_reserve0, _reserve1);逻辑:若协议手续费开启(0.05%),检查 kLast(上次手续费结算时的 reserve0*reserve1)与当前储备金的差值。若有手续费收益,铸造对应 LP 代币给工厂合约。影响:手续费会略微增加 totalSupply,从而影响后续 LP 销毁计算。3. 计算应返还的代币量amount0 = liquidity.mul(balance0) / _totalSupply; amount1 = liquidity.mul(balance1) / _totalSupply; require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED');数学公式:关键点:使用 当前余额(balance0) 而非 储备金(_reserve0),确保包含未结算的手续费,实现按比例公平分配。必须返还两种代币(防止只提取单一代币的攻击)。4. 销毁 LP 并转账代币_burn(address(this), liquidity); // 销毁LP _safeTransfer(_token0, to, amount0); // 转代币0给用户 _safeTransfer(_token1, to, amount1); // 转代币1给用户原子性:先销毁 LP,再转账代币,防止重入攻击。安全转账:_safeTransfer 会验证代币合约的返回值,防止恶意代币导致资金锁定。5. 更新储备金与触发事件balance0 = IERC20(_token0).balanceOf(address(this)); // 更新余额 balance1 = IERC20(_token1).balanceOf(address(this)); _update(balance0, balance1, _reserve0, _reserve1); // 同步储备金 if (feeOn) kLast = uint(reserve0).mul(reserve1); // 更新kLast emit Burn(msg.sender, amount0, amount1, to); // 事件日志_update:将最新余额写入储备金,并计算累计价格(用于 TWAP 预言机)。kLast:仅在手续费开启时更新,作为下次手续费计算的基准。分叉主网测试removeLiquiditycontract UniswapV2LiquidityTest is Test { IWETH private constant weth = IWETH(WETH); IERC20 private constant dai = IERC20(DAI); IUniswapV2Router02 private constant router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02); IUniswapV2Pair private constant pair = IUniswapV2Pair(UNISWAP_V2_PAIR_DAI_WETH); address private constant user = address(100); function setUp() public { // Fund WETH to user deal(user, 100 * 1e18); vm.startPrank(user); weth.deposit{value: 100 * 1e18}(); weth.approve(address(router), type(uint256).max); vm.stopPrank(); // Fund DAI to user deal(DAI, user, 1000000 * 1e18); vm.startPrank(user); dai.approve(address(router), type(uint256).max); vm.stopPrank(); } function test_removeLiquidity() public { vm.startPrank(user); (,, uint256 liquidity) = router.addLiquidity({ tokenA: DAI, tokenB: WETH, amountADesired: 1000000 * 1e18, amountBDesired: 100 * 1e18, amountAMin: 1, amountBMin: 1, to: user, deadline: block.timestamp }); pair.approve(address(router), liquidity); (uint256 amountA, uint256 amountB) = router.removeLiquidity(WETH, DAI, liquidity, 1, 1, user, block.timestamp); vm.stopPrank(); console2.log("DAI:", amountA); console2.log("WETH:", amountB); assertEq(pair.balanceOf(user), 0, "LP = 0"); } }
2025年08月05日
1 阅读
0 评论
1 点赞
2025-08-02
Uniswap v2-Swap
Uniswap v2一、Swap单个币种之间swap的流程多个币种之间swap的流程1.swapExactTokensForTokens 和 swapTokensForExactTokens这个函数是 Uniswap V2 路由器合约中的一个重要功能,用于将确切数量的某种代币交换为另一种代币(可能经过多个交易对)。// v2-periphery/contracts/UniswapV2Router02.sol // swapExactTokensForTokens (精确输入,不确定输出) // 功能:用确切数量的输入代币换取尽可能多的输出代币 function swapExactTokensForTokens( uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline ) external virtual override ensure(deadline) returns (uint[] memory amounts) { amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); TransferHelper.safeTransferFrom( path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] ); _swap(amounts, path, to); } // swapTokensForExactTokens 精确输出,不确定输入 // 用尽可能少的输入代币换取确切数量的输出代币 function swapTokensForExactTokens( uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline ) external virtual override ensure(deadline) returns (uint[] memory amounts) { amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); TransferHelper.safeTransferFrom( path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0] ); _swap(amounts, path, to); }参数解释amountIn: 你想交换的输入代币数量amountOutMin: 你愿意接受的最小输出代币数量(防止高滑点)path: 交换路径(代币地址数组)to: 接收输出代币的地址deadline: 交易有效期限(Unix 时间戳)path 参数path 数组表示从代币 A 到代币 B 可能经过的所有中间代币。例如:直接交换:[代币A, 代币B]通过中间代币交换:[代币A, 代币WETH, 代币B]函数会按照路径顺序一一交换,直到最后一个目标币种。factory 参数factory 确实是指 UniswapV2Factory 合约(核心合约之一),它负责创建和管理交易对。amounts 数组amounts 是通过 UniswapV2Library.getAmountsOut() 计算得到的数组,它包含:沿交换路径每个阶段预期的代币数量数组的第一个元素是 amountIn(输入数量)最后一个元素是预期的输出代币数量中间元素是每个中间交换步骤的预期数量例如,对于路径 [A, B, C],amounts 可能看起来像 [100 A, 50 B, 25 C],表示:用 100 A 换 50 B再用 50 B 换 25 C函数执行流程计算沿路径的预期输出量 (getAmountsOut)检查最终输出是否满足用户的最小要求 (amountOutMin)将输入代币从用户转移到第一个交易对执行沿路径的交换 (_swap 内部函数)返回各阶段的实际交换数量这种设计允许复杂的多跳交换,同时确保用户获得至少他们指定的最小输出量。分叉主网测试getAmountsOut和getAmountsInforge test --fork-url $FORK_URL -vvv// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {Test, console2} from "forge-std/Test.sol"; import {IERC20} from "../../src/interfaces/IERC20.sol"; import {IWETH} from "../../src/interfaces/IWETH.sol"; import {IUniswapV2Router02} from "../../src/interfaces/uniswap-v2/IUniswapV2Router02.sol"; import {DAI, WETH, MKR, UNISWAP_V2_ROUTER_02} from "../../src/Constants.sol"; contract UniswapV2SwapAmountsTest is Test { IWETH private constant weth = IWETH(WETH); IERC20 private constant dai = IERC20(DAI); IERC20 private constant mkr = IERC20(MKR); IUniswapV2Router02 private constant router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02); function test_getAmountsOut() public { address[] memory path = new address[](3); path[0] = WETH; path[1] = DAI; path[2] = MKR; uint256 amountIn = 1e18; uint256[] memory amounts = router.getAmountsOut(amountIn, path); console2.log("WETH", amounts[0]); console2.log("DAI", amounts[1]); console2.log("MKR", amounts[2]); } function test_getAmountsIn() public { address[] memory path = new address[](3); path[0] = WETH; path[1] = DAI; path[2] = MKR; uint256 amountOut = 1e16; uint256[] memory amounts = router.getAmountsIn(amountOut, path); console2.log("WETH", amounts[0]); console2.log("DAI", amounts[1]); console2.log("MKR", amounts[2]); } } 2._swap最终执行两个token之间转换的核心函数 // v2-periphery/contracts/UniswapV2Router02.sol function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual { for (uint i; i < path.length - 1; i++) { (address input, address output) = (path[i], path[i + 1]); (address token0,) = UniswapV2Library.sortTokens(input, output); uint amountOut = amounts[i + 1]; (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0)); address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to; IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap( amount0Out, amount1Out, to, new bytes(0) ); } }函数参数amounts:交易路径上每个代币的预期数量数组(由 getAmountsOut 计算得出)path:代币交换路径(如 [代币A, 代币B, 代币C])_to:最终接收代币的地址核心逻辑分步解析步骤1:遍历交易路径for (uint i; i < path.length - 1; i++) { (address input, address output) = (path[i], path[i + 1]);循环处理路径中的每一跳(如 A→B→C 需要处理 A→B 和 B→C 两跳)input 和 output 分别表示当前交易的输入代币和输出代币步骤2:排序代币确定交易对(address token0,) = UniswapV2Library.sortTokens(input, output);Uniswap 交易对中的代币按地址排序存储(token0 < token1)通过排序确定代币在交易对中的顺序(影响 amount0Out 和 amount1Out 的赋值)步骤3:确定输出量uint amountOut = amounts[i + 1]; (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));amountOut 是当前交易对的预期输出量(来自 amounts 数组)关键规则:如果 input 是 token0,则 amount0Out = 0,amount1Out = amountOut(因为输入是 token0,输出是 token1)如果 input 是 token1,则 amount0Out = amountOut,amount1Out = 0(输入是 token1,输出是 token0)步骤4:确定接收地址address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;如果是中间交易(如 A→B→C 中的 A→B):to 是下一个交易对的地址(B/C 的交易对)如果是最后一跳(如 B→C):to 是用户指定的最终地址 _to步骤5:执行交易IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap( amount0Out, amount1Out, to, new bytes(0) );通过 pairFor 计算当前交易对的地址调用交易对的 swap 方法,传递输出量和接收地址new bytes(0) 表示无回调数据(非闪电贷场景)_swap调用了IUniswapV2Pair的pairFor.swap函数// v2-core/contracts/UniswapV2Pair.sol // this low-level function should be called from a contract which performs important safety checks function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT'); (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY'); uint balance0; uint balance1; { // scope for _token{0,1}, avoids stack too deep errors address _token0 = token0; address _token1 = token1; require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); } uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT'); { // scope for reserve{0,1}Adjusted, avoids stack too deep errors uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3)); uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3)); require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); } _update(balance0, balance1, _reserve0, _reserve1); emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to); }函数参数amount0Out:从储备中提取的 token0 数量amount1Out:从储备中提取的 token1 数量to:接收输出代币的地址data:回调数据(用于闪电贷等高级操作)主要逻辑步骤初步检查require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');确保至少有一个输出量大于0(不能两个都为0)使用getReserves来获取状态变量 在同一个快照时间获取数据保持数据一致性 同时这三个变量由于在一个存储槽内会被打包成一个sload操作节省gasuint112 private reserve0; // uses single storage slot, accessible via getReserves uint112 private reserve1; // uses single storage slot, accessible via getReserves uint32 private blockTimestampLast;为了安全会提前检查此时需要swap的两个token目前的余量require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');执行转账这块代码中使用了很多花括号 创建一个新的作用域(Scope),它可以用来 临时限制变量的生命周期,从而帮助解决 "Stack Too Deep"(堆栈过深) 的错误。Solidity 的 EVM 使用 栈(Stack) 来存储局部变量,但 栈的最大深度限制是 16 个槽位(即最多同时存储 16 个局部变量)。{ ... } 内部定义的变量只在块内有效,退出块后,这些变量 不再占用栈空间。这样,编译器可以 复用栈槽位,而不是持续占用新的位置。除了 { ... } 作用域隔离,还可以:使用 memory 或 calldata 存储临时数据(减少栈使用)。拆分成多个函数(减少单个函数的变量数量)。使用结构体(struct)或数组 打包变量(但可能增加 Gas 成本)。内联汇编(assembly) 手动管理栈(高级用法)。{ // scope for _token{0,1}, avoids stack too deep errors address _token0 = token0; address _token1 = token1; require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); }获取代币地址确保接收地址不是代币合约本身执行输出代币的转账(乐观转账)处理回调(闪电贷)if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);如果有回调数据,通知接收者(用于闪电贷等场景)计算实际输入量balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;获取新的余额计算实际输入量:如果新余额 > (旧储备 - 输出量),差额就是输入量否则输入量为0先转账后检测,swap之前先讲要转换的token转账到币对合约兼容 ERC-20 标准:ERC-20 的 transferFrom 需要单独授权和转账操作,无法在单次调用中同时完成转入和交换。安全性:强制用户先转账可以防止重入攻击(转账和交换分离)。检查恒定乘积公式uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3)); uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3)); require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');计算调整后的余额(考虑0.3%手续费)确保 (adjustedBalance0 × adjustedBalance1) ≥ (旧储备0 × 旧储备1)这是Uniswap恒定乘积公式的核心检查更新状态_update(balance0, balance1, _reserve0, _reserve1); emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);更新储备量触发Swap事件function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW'); uint32 blockTimestamp = uint32(block.timestamp % 2**32); uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { // * never overflows, and + overflow is desired price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; } reserve0 = uint112(balance0); reserve1 = uint112(balance1); blockTimestampLast = blockTimestamp; emit Sync(reserve0, reserve1); }(1) 溢出检查require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');作用:确保余额不超过 uint112 的最大值($2^{112}-1$)。uint112(-1) 是 Solidity 中获取该类型最大值的方式(等价于 type(uint112).max)。(2) 时间处理uint32 blockTimestamp = uint32(block.timestamp % 2**32); uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired时间截断:block.timestamp % 2**32 确保时间戳在 uint32 范围内(约 136 年循环一次)。时间差计算:timeElapsed 允许溢出(如时间戳回绕时),因为 Uniswap 依赖时间差模运算的正确性。例如:若 blockTimestampLast = 2^32-1,blockTimestamp = 0,则 timeElapsed = 1(符合预期)。(3) 累计价格更新if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; }触发条件:时间已流逝(timeElapsed > 0)。储备金非零(防止除零错误)。价格计算:price0CumulativeLast:记录 token1/token0 的累计价格(单位为秒)。使用 UQ112x112 定点数库保持精度(112 位整数 + 112 位小数)。price1CumulativeLast:同理记录 token0/token1 的价格。为什么需要时间加权?抗操纵性市场价格可能被瞬时操纵(如闪电贷攻击)。TWAP 通过长时间累积平滑价格波动,外部合约可查询两个时间点的累计价差,再除以时间差得到平均价格:TWAP = priceCumulative(t2)−priceCumulative(t1) / (t2−t1)设计目的:支持链下价格预言机(如 TWAP 时间加权平均价格)。累计价格随时间线性增长,外部合约可通过差值计算时间段内的平均价格。(4) 储备金和时间戳更新reserve0 = uint112(balance0); reserve1 = uint112(balance1); blockTimestampLast = blockTimestamp; emit Sync(reserve0, reserve1);更新状态:将实时余额写入储备金(reserve0/reserve1)。记录当前时间戳(blockTimestampLast)。事件:触发 Sync 事件,通知外部监听者储备金变化。分叉主网测试swapExactTokensForTokens和swapTokensForExactTokens// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import {Test, console2} from "forge-std/Test.sol"; import {IERC20} from "../src/interfaces/IERC20.sol"; import {IWETH} from "../src/interfaces/IWETH.sol"; import {IUniswapV2Router02} from "../src/interfaces/uniswap-v2/IUniswapV2Router02.sol"; import {IUniswapV2Pair} from "../src/interfaces/uniswap-v2/IUniswapV2Pair.sol"; import {DAI, WETH, MKR, UNISWAP_V2_PAIR_DAI_MKR, UNISWAP_V2_ROUTER_02} from "../../../src/Constants.sol"; contract UniswapV2SwapTest is Test { IWETH private constant weth = IWETH(WETH); IERC20 private constant dai = IERC20(DAI); IERC20 private constant mkr = IERC20(MKR); IUniswapV2Router02 private constant router = IUniswapV2Router02(UNISWAP_V2_ROUTER_02); IUniswapV2Pair private constant pair = IUniswapV2Pair(UNISWAP_V2_PAIR_DAI_MKR); address private constant user = address(100); function setUp() public { deal(user, 100 * 1e18); vm.startPrank(user); weth.deposit{value: 100 * 1e18}(); weth.approve(address(router), type(uint256).max); vm.stopPrank(); // Add MKR liquidity to DAI/MKR pool deal(DAI, address(pair), 1e6 * 1e18); deal(MKR, address(pair), 1e5 * 1e18); pair.sync(); } // Swap all input tokens for as many output tokens as possible function test_swapExactTokensForTokens() public { address[] memory path = new address[](3); path[0] = WETH; path[1] = DAI; path[2] = MKR; uint256 amountIn = 1e18; uint256 amountOutMin = 1; vm.prank(user); uint256[] memory amounts = router.swapExactTokensForTokens({ amountIn: amountIn, amountOutMin: amountOutMin, path: path, to: user, deadline: block.timestamp }); console2.log("WETH", amounts[0]); console2.log("DAI", amounts[1]); console2.log("MKR", amounts[2]); assertGe(mkr.balanceOf(user), amountOutMin, "MKR balance of user"); } // Receive an exact amount of output tokens for as few input tokens // as possible function test_swapTokensForExactTokens() public { address[] memory path = new address[](3); path[0] = WETH; path[1] = DAI; path[2] = MKR; uint256 amountOut = 0.1 * 1e18; uint256 amountInMax = 1e18; vm.prank(user); uint256[] memory amounts = router.swapTokensForExactTokens({ amountOut: amountOut, amountInMax: amountInMax, path: path, to: user, deadline: block.timestamp }); console2.log("WETH", amounts[0]); console2.log("DAI", amounts[1]); console2.log("MKR", amounts[2]); assertEq(mkr.balanceOf(user), amountOut, "MKR balance of user"); } }
2025年08月02日
1 阅读
0 评论
1 点赞
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-22
About the EVM Part II
8.控制流(Control Flow)指令以太坊虚拟机(EVM)中的控制流指令用于控制程序执行的流程。这类指令不涉及算术计算或内存操作,而是改变程序计数器(Program Counter,pc)的值,从而跳转到新的位置继续执行。以下是常用的 5 个控制流指令及其详细说明(包含源码级别原理和 Gas 消耗):🛑 1. STOP(0x00)功能:终止当前执行,正常返回。源码实现:func opStop(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { return nil, errStopToken }行为:不消耗任何栈、内存等资源。直接停止程序执行。Gas 消耗:0 Gas🔁 2. JUMP(0x56)功能:跳转到某个位置执行。前提:跳转目标必须是 JUMPDEST 指令的地址,否则异常。栈行为:栈顶:跳转地址源码实现:func opJump(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { if interpreter.evm.abort.Load() { return nil, errStopToken } pos := scope.Stack.pop() if !scope.Contract.validJumpdest(&pos) { return nil, ErrInvalidJump } *pc = pos.Uint64() - 1 // pc will be increased by the interpreter loop return nil, nil }validJumpdest 会检查该地址是否是 JUMPDEST(0x5b)字节。由于 pc 在每条指令之后自动加 1,所以此处设置为 dest - 1。Gas 消耗:8 Gas❓ 3. JUMPI(0x57)功能:条件跳转,只有条件非零才跳转。栈行为:栈顶:condition 次栈顶:destination源码实现:func opJumpi(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { dest, cond := scope.Stack.pop(), scope.Stack.pop() if cond.IsZero() { return nil, nil // 不跳转 } if !interpreter.validJumpdest(dest.Uint64(), scope.Contract.Code) { return nil, ErrInvalidJump } *pc = dest.Uint64() - 1 return nil, nil }行为说明:如果 cond == 0,什么也不做。如果 cond != 0,执行跳转到 dest。Gas 消耗:10 Gas🎯 4. JUMPDEST(0x5b)功能:跳转目标的标记,标志合法的 JUMP/JUMPI 可跳转的位置。源码实现:func opJumpdest(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { return nil, nil }行为:什么都不做,就是个占位符。编译器在生成代码时会在跳转目标位置插入 JUMPDEST。Gas 消耗:1 Gas📍 5. PC(0x58)功能:将当前指令的程序计数器 pc 压入栈。栈行为:push 当前 PC源码实现:func opPc(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { scope.Stack.push(new(uint256.Int).SetUint64(*pc)) return nil, nil }用途:常用于合约内部的程序构造(如 inline jump)。与 JUMP 配合使用时可构造动态跳转。Gas 消耗:2 Gas🔍 总结对比表:指令含义栈操作Gas 消耗说明STOP停止执行无0不返回值,正常终止JUMP跳转弹出目标地址8目标地址必须是 JUMPDESTJUMPI条件跳转弹出地址和条件10条件非零跳转JUMPDEST跳转目标标记无1合法跳转的目标PC获取当前指令地址压入当前 pc2可用于构造 jump 表9.查询区块信息以太坊虚拟机(EVM)中用于查询区块信息的 9 个指令的详细说明,包括其功能、Gas 消耗以及简要的源码实现原理。📦 区块信息相关指令一览指令名操作码功能描述Gas 消耗栈操作(输入 → 输出)BLOCKHASH0x40获取指定区块号的哈希(最近 256 个区块)20blockNumber → blockHashCOINBASE0x41当前区块的矿工地址2→ addressTIMESTAMP0x42当前区块的时间戳2→ timestampNUMBER0x43当前区块号2→ blockNumberPREVRANDAO0x44上一个区块的随机数(Beacon 链)2→ randomnessGASLIMIT0x45当前区块的 Gas 上限2→ gasLimitCHAINID0x46当前链的 Chain ID2→ chainIdBASEFEE0x48当前区块的 Base Fee2→ baseFeeBLOBBASEFEE0x4A当前区块的 Blob Base Fee(EIP-7516)2→ blobBaseFee🔍 指令详解与源码实现概述1. BLOCKHASH(0x40)功能:获取指定区块号的哈希,仅限于最近 256 个区块。Gas 消耗:20源码实现概述:从栈中弹出区块号。调用 GetHash(blockNumber) 方法获取对应的区块哈希。将结果压入栈中。2. COINBASE(0x41)功能:返回当前区块的矿工地址。Gas 消耗:2源码实现概述:直接从区块上下文中读取 Coinbase 字段。将结果压入栈中。3. TIMESTAMP(0x42)功能:返回当前区块的时间戳。Gas 消耗:2源码实现概述:从区块上下文中读取 Time 字段。将结果压入栈中。Solidity Developer4. NUMBER(0x43)功能:返回当前区块号。Gas 消耗:2源码实现概述:从区块上下文中读取 Number 字段。将结果压入栈中。5. PREVRANDAO(0x44)功能:返回上一个区块的随机数(适用于以太坊 2.0)。Gas 消耗:2源码实现概述:从区块上下文中读取 PrevRandao 字段。将结果压入栈中。6. GASLIMIT(0x45)功能:返回当前区块的 Gas 上限。Gas 消耗:2源码实现概述:从区块上下文中读取 GasLimit 字段。将结果压入栈中。Ethereum Stack Exchange7. CHAINID(0x46)功能:返回当前链的 Chain ID。Gas 消耗:2源码实现概述:从链配置中读取 ChainID。将结果压入栈中。GitHub+4以太坊+4GitHub+48. BASEFEE(0x48)功能:返回当前区块的 Base Fee。Gas 消耗:2源码实现概述:从区块上下文中读取 BaseFee 字段。将结果压入栈中。Secure Contracts+2以太坊+2ZKsync Docs+29. BLOBBASEFEE(0x4A)功能:返回当前区块的 Blob Base Fee(EIP-7516)。Gas 消耗:2源码实现概述:从区块上下文中读取 BlobBaseFee 字段。将结果压入栈中。Abstract+6以太坊+6GitHub+610.账户 Account 相关EVM 中与 账户 Account 相关的 4 个指令分别是:BALANCE (0x31)EXTCODESIZE (0x3B)EXTCODECOPY (0x3C)EXTCODEHASH (0x3F)在以太坊的 Geth 实现中,每个账户(外部账户或合约账户)在执行期间被建模为一个 stateObject,保存在 StateDB.stateObjects 这个 map 中,其 key 是账户地址,value 就是 *stateObject🔍 stateObject 结构字段详解👉 基本信息字段字段类型说明db*StateDB关联的状态数据库,提供持久化和状态读取功能addresscommon.Address当前账户地址(20 字节)addrHashcommon.Hash当前地址的 keccak256 哈希,作为 MPT 中的键origin*types.StateAccount初始状态副本(还没被当前 block 修改的快照)datatypes.StateAccount当前状态数据,包含变更后余额、nonce、storage root、code hash其中,types.StateAccount 是这样定义的:type StateAccount struct { Nonce uint64 Balance *uint256.Int Root common.Hash // storage trie 的根哈希 CodeHash []byte // 合约字节码的哈希值 }👉 存储相关字段(Storage Tries)以太坊合约的每个 storage 都是一个单独的 Merkle Patricia Trie。字段类型说明trieTrie 接口当前账户的 storage trie(延迟加载,首次访问时才初始化)originStorageStorage(map[Hash]Hash)当前区块中曾读取过的存储项,原始值dirtyStorageStorage当前交易中被写入的存储项(临时更改)pendingStorageStorage当前区块中写入但尚未 commit 的存储项uncommittedStorageStorageByzantium 之后,追踪自 block 开始以来的所有修改(辅助合约重入和 Gas Refund)这些字段的存在是为了:加速访问:减少 trie 查询频率支持事务隔离:交易提交前不影响其他交易支持 fork 条件下状态回滚与恢复⚠️ 特别注意:只有 commit 之后,才会写入 storage trie,并最终持久化到 disk 上的 LevelDB。👉 合约代码缓存相关字段类型说明code[]byte缓存的合约字节码(由 CodeHash 指向)dirtyCodebool合约代码是否在当前交易中被修改过(用于判断是否需要更新 CodeHash)合约代码是不可变的,因此通常只在合约部署时写入一次。读取时会从 CodeHash 中查找缓存或 LevelDB 中的代码。👉 Self-Destruct 与合约创建状态字段类型说明selfDestructedbool是否在当前交易中触发了 SELFDESTRUCT,但还没彻底清理newContractbool是否是新部署的合约(用于 EIP-6780 控制 selfdestruct 规则)selfDestructed 为 true 的账户在交易内仍然可访问,但交易结束后会被移除(余额归属给指定地址)。newContract 是为了解决 EIP-6780 提出的新规则 —— 并非所有合约都能 selfdestruct。📌 stateObject 的作用概括作用说明状态建模描述一个账户的完整状态(余额、nonce、storage、code)执行隔离支持事务级别的变更记录、撤销和写入持久化桥梁从内存(变更)到 MPT(trie 节点)再到 LevelDBGas 计算通过状态变更辅助计算 Gas(如 storage 修改、code 更新)Fork 支持支持区块级与交易级的状态快照、回滚和恢复🧠 额外说明:Storage 的生命周期阶段操作写入位置读取SLOADoriginStorage写入SSTOREdirtyStorage(交易级)→ pendingStorage(区块级)提交tx commit把 pendingStorage 提交到 storage trie(trie)区块 commitblock commit把所有账户的 trie 根更新到 global state trie 的节点下面我将分别给出它们在 Go-Ethereum 中的源码实现(关键代码段)、功能、Gas 消耗和行为解释。✅ 1. BALANCE (0x31)📌 功能返回指定地址的账户余额(以 wei 为单位)⛽ Gas 消耗Cold access: 2600(EIP-2929)Warm access: 100🧠 Go 源码func opBalance(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { slot := scope.Stack.peek() address := common.Address(slot.Bytes20()) slot.Set(interpreter.evm.StateDB.GetBalance(address)) return nil, nil } func (s *StateDB) GetBalance(addr common.Address) *uint256.Int { stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.Balance() } return common.U2560 } func (s *stateObject) Balance() *uint256.Int { return s.data.Balance } type stateObject struct { db *StateDB address common.Address // address of ethereum account addrHash common.Hash // hash of ethereum address of the account origin *types.StateAccount // Account original data without any change applied, nil means it was not existent data types.StateAccount // Account data with all mutations applied in the scope of block ... } type StateAccount struct { Nonce uint64 Balance *uint256.Int Root common.Hash // merkle root of the storage trie CodeHash []byte }🔍 说明scope.Stack 是当前 EVM 执行上下文的栈。peek() 获取栈顶元素(并不弹出)。通过 Bytes20() 将其强制转换成 20 字节(以太坊地址)。然后包装为 common.Address。getStateObject 会尝试从状态树或缓存中获取地址对应的账户(stateObject)。如果存在,调用 Balance()。如果地址不存在(合约或账户从未出现过),则余额视为 0(common.U2560 是 uint256 的 0 值)。stateObject.data 是当前账户的状态快照(类型 StateAccount)。其中 Balance 是个 *uint256.Int 指针,存储账户余额。✅ 2. EXTCODESIZE (0x3B)📌 功能返回某个地址的合约字节码长度⛽ Gas 消耗Cold access: 2600(EIP-2929)Warm access: 100🧠 Go 源码func opExtCodeSize(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { slot := scope.Stack.peek() slot.SetUint64(uint64(interpreter.evm.StateDB.GetCodeSize(slot.Bytes20()))) return nil, nil }🔍 说明从 StateDB.GetCodeSize(addr) 获取代码字节数组长度✅ 3. EXTCODECOPY (0x3C)📌 功能将指定地址的合约代码复制到内存中(用于读取部分代码)⛽ Gas 消耗Base: 100Per word copied: 3 × ceil(size / 32)Cold access penalty: +2600(如果是冷访问)🧠 Go 源码func opExtCodeCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { var ( memOffset = scope.Stack.pop() codeOffset = scope.Stack.pop() length = scope.Stack.pop() ) uint64CodeOffset, overflow := codeOffset.Uint64WithOverflow() if overflow { uint64CodeOffset = math.MaxUint64 } codeCopy := getData(scope.Contract.Code, uint64CodeOffset, length.Uint64()) scope.Memory.Set(memOffset.Uint64(), length.Uint64(), codeCopy) return nil, nil }🔍 说明从 EVM 操作数栈中依次弹出:memOffset: 写入内存的起始偏移位置codeOffset: 要拷贝的代码段的起始位置length: 要拷贝的字节数代码偏移量从大整数转换为 uint64,若超出范围就使用最大值。防止恶意代码传入极大值导致 panic。从目标账户的代码中提取指定区间的字节(类似切片截取)如果越界了,EVM 要求 padding 为 0(填零)。append(..., make(...)) 是为了避免 panic 并补足返回值用于代理合约(Proxy)判断目标地址是否有代码。用于合约间交互时动态读取合约逻辑。用于合约内通过代码检查是否是合约地址(如 EIP-1167 的 minimal proxy)。✅ 4. EXTCODEHASH (0x3F)📌 功能返回指定地址代码的 keccak256(code),如果该地址没有代码,则返回 0⛽ Gas 消耗Cold access: 2600(EIP-1052)Warm access: 100🧠 Go 源码func opExtCodeHash(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { slot := scope.Stack.peek() address := common.Address(slot.Bytes20()) if interpreter.evm.StateDB.Empty(address) { slot.Clear() } else { slot.SetBytes(interpreter.evm.StateDB.GetCodeHash(address).Bytes()) } return nil, nil }🔍 说明从 StateDB 获取该账户的 代码哈希(通常是 Keccak256),并设置到栈顶。📊 总结对比表指令功能Gas(Cold/Warm)依赖 StateDB 方法BALANCE账户余额2600 / 100GetBalance, AccessAccountEXTCODESIZE合约代码长度2600 / 100GetCode, AccessAccountEXTCODECOPY复制合约代码到内存2600 + 3/wordGetCode, AccessAccountEXTCODEHASH合约代码哈希2600 / 100GetCodeHash, HasCode11.交易相关EVM 中与 交易上下文(Transaction Context) 相关的 4 个核心指令包括:指令功能ADDRESS当前合约地址ORIGIN外部交易发起方地址(EOA)CALLER当前调用者地址CALLVALUE调用时附带的 msg.value 数值下面将详细从 最新 Geth 源码(v1.14.x) 层面解析这些指令的逻辑实现,以及它们的 Gas 消耗。🔹 1. ADDRESS✅ 功能返回当前执行合约的地址(address(this))📘 源码实现func opAddress(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { scope.Stack.push(new(uint256.Int).SetBytes(scope.Contract.Address().Bytes())) return nil, nil }解释:scope.Contract 是当前正在执行的合约实例。Address() 返回其地址,20 字节压入栈顶。⛽ Gas 消耗操作GasADDRESS2🔹 2. ORIGIN✅ 功能返回最初交易发起者地址(一个 EOA),即 tx.origin📘 源码实现func opOrigin(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { scope.Stack.push(new(uint256.Int).SetBytes(interpreter.evm.Origin.Bytes())) return nil, nil }解释:interpreter.evm.Origin 在执行上下文初始化时设置。通常为最初的 EOA(Externally Owned Account)地址。⛽ Gas 消耗操作GasORIGIN2🔹 3. CALLER✅ 功能返回当前消息调用者(可以是合约或EOA),即 msg.sender📘 源码实现func opCaller(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { scope.Stack.push(new(uint256.Int).SetBytes(scope.Contract.Caller().Bytes())) return nil, nil }解释:如果合约 A 调用合约 B,则 msg.sender 为 A。scope.Contract.Caller() 即当前调用者地址。⛽ Gas 消耗操作GasCALLER2🔹 4. CALLVALUE✅ 功能返回当前调用时附带的以 wei 为单位的金额,即 msg.value📘 源码实现func opCallValue(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { scope.Stack.push(new(uint256.Int).Set(scope.Contract.value)) return nil, nil }解释:scope.Contract.value 表示此次消息调用的 ETH 附带值。是一个 *uint256.Int 表示的数值。⛽ Gas 消耗操作GasCALLVALUE2✅ 汇总对比表格指令功能栈操作来源字段GasADDRESS当前合约地址push(address)scope.Contract.Address()2ORIGIN交易原始发起者(EOA)push(tx.origin)interpreter.evm.Origin2CALLER调用者地址(msg.sender)push(msg.sender)scope.Contract.Caller()2CALLVALUE附带 ETH 值(msg.value)push(msg.value)scope.Contract.value212.日志相关EVM 中与日志(Log)相关的指令是:指令名说明LOG0不带 topic 的日志LOG11 个 topicLOG22 个 topicLOG33 个 topicLOG44 个 topic(最多)这些指令由智能合约调用 LOGx 指令时触发,最终写入 TransactionReceipt 的 logs 字段中。📦 Golang 源码位置路径:core/vm/instructions.go以 LOG0 为例,实际实现是共用一个函数:func makeLog(size int) executionFunc { return func(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { if interpreter.readOnly { return nil, ErrWriteProtection } topics := make([]common.Hash, size) stack := scope.Stack mStart, mSize := stack.pop(), stack.pop() for i := 0; i < size; i++ { addr := stack.pop() topics[i] = addr.Bytes32() } d := scope.Memory.GetCopy(mStart.Uint64(), mSize.Uint64()) interpreter.evm.StateDB.AddLog(&types.Log{ Address: scope.Contract.Address(), Topics: topics, Data: d, // This is a non-consensus field, but assigned here because // core/state doesn't know the current block number. BlockNumber: interpreter.evm.Context.BlockNumber.Uint64(), }) return nil, nil } }在初始化指令集时,会这样注册: LOG0: { execute: makeLog(0), dynamicGas: makeGasLog(0), minStack: minStack(2, 0), maxStack: maxStack(2, 0), memorySize: memoryLog, }, LOG1: { execute: makeLog(1), dynamicGas: makeGasLog(1), minStack: minStack(3, 0), maxStack: maxStack(3, 0), memorySize: memoryLog, }, LOG2: { execute: makeLog(2), dynamicGas: makeGasLog(2), minStack: minStack(4, 0), maxStack: maxStack(4, 0), memorySize: memoryLog, }, LOG3: { execute: makeLog(3), dynamicGas: makeGasLog(3), minStack: minStack(5, 0), maxStack: maxStack(5, 0), memorySize: memoryLog, }, LOG4: { execute: makeLog(4), dynamicGas: makeGasLog(4), minStack: minStack(6, 0), maxStack: maxStack(6, 0), memorySize: memoryLog, },🔍 详细解析❶ mStart, mSize:从合约内存中提取数据的起始位置和长度。❷ topics:根据 LOGx 中的 x,从栈中弹出对应数量的 topic。❸ data:从合约内存中 GetCopy 取出 mSize 字节数据,这就是事件的 data 部分。❹ 调用 StateDB.AddLog:这会将日志加入 logs 中,最终附着在交易回执(TransactionReceipt)中,供事件监听器或前端读取。⛽ Gas 消耗在 params\protocol_params.go 中定义:LogGas uint64 = 375 // Per LOG* operation.🔢 LOGx 的 Gas 消耗计算方式:Gas = memoryGasCost(mem, memorySize) + 375(基础) + 375 * topic 数 + 8 * data 长度(按字节)举例:LOG2,有 2 个 topic,日志数据长 100 字节:Gas = memoryGasCost(mem, memorySize) + 375 + 375×2 + 8×100 = 375 + 750 + 800 = 1925✅ 总结表格指令Topics 数栈项数功能Gas 构成LOG002写入 datamemoryGasCost(mem, memorySize) + 375 + 8×dataLenLOG113写入 1 topic + datamemoryGasCost(mem, memorySize) + 375 + 375×1 + 8×dataLenLOG224写入 2 topics + datamemoryGasCost(mem, memorySize) + 375 + 375×2 + 8×dataLenLOG335写入 3 topics + datamemoryGasCost(mem, memorySize) + 375 + 375×3 + 8×dataLenLOG446写入 4 topics + datamemoryGasCost(mem, memorySize) + 375 + 375×4 + 8×dataLen🧠 实战补充合约中通常使用:event Transfer(address indexed from, address indexed to, uint256 amount); emit Transfer(msg.sender, to, value);EVM 将此编译为 LOG3(事件签名 hash + 2 indexed topics)。使用LOG3是因为函数选择器会被默认作为一个topicLOG0 的事件是无法被事件过滤器(filter)快速索引的,记录不需要索引的调试信息或原始数据事件声明indexed 数量EVM LOG 指令实际 topic 数量(包含 selector)event RawData(bytes data) anonymous;0LOG00event A(uint256 value);0LOG11(只有事件 selector)event B(address indexed from);1LOG22(selector + from)event C(address indexed a, address indexed b);2LOG33(selector + a + b)event D(a indexed, b indexed, c indexed);3LOG44(selector + a + b + c)📌 布隆过滤器在 Ethereum 中的作用🔍 主要用于:加速事件日志的检索当你在使用 Web3.js、ethers.js 或者通过区块浏览器(例如 Etherscan)查询事件时,底层会:查询区块头中的布隆过滤器(logsBloom 字段)。检查事件的 topic 是否可能出现在该区块中。如果布隆过滤器中没命中,就一定没有这个事件,就不用再查日志详情了; 如果命中了,再去读取区块中完整的日志,逐条精确匹配。🧠 举个例子你查:contract.filters.Transfer(address1, null)想查找某个地址是否发送过 Transfer 事件。查询每个区块的 logsBloom:如果 logsBloom 中没有这个地址(作为 topic 编码),那就不用继续查这个区块了。如果有,再去深入查 logs(日志项),看有没有匹配的 Transfer 事件。📦 EVM 的布隆过滤器结构每个区块头中有个字段:logsBloom,大小是:256 字节 = 2048 bit为每个日志的 topic 和地址计算哈希(Keccak256),提取若干 bit 位置标记为 1布隆过滤器构造时大致流程(简化):bloom := [256]byte{} // 2048 bits // 对每个 topic/address 做 keccak256,提取 3 个位置置 1 positions := getBloomBits(keccak256(data)) for pos in positions { bloom[pos] = 1 }✅ 总结:Ethereum 中日志用布隆过滤器的目的功能描述🌱 过滤加速快速跳过不包含目标事件的区块,节省读取时间⚡ 快速索引只要 logsBloom 未命中,就无需解析区块体📊 控制开销减少节点为每个请求都加载全量日志的成本🧪 概率保障不会漏查存在的事件,但可能多查几个误判的区块(少量)
2025年05月22日
4 阅读
0 评论
2 点赞
1
...
5
6
7