<100 subscribers

Uniswap V3交易预计算技巧
Uniswap上做swap交易时,比如用usdt购买btc,会根据界面上输入的usdt数量,实时计算出可以swap到多少个btc,v2版本因为是应用了xy=k的公式,可以方便的计算出来。代码里通过getAmountOut和getAmountIn得到,这两个都是view函数,不需要消耗gas。而到了...
Meebits mint随机算法
先上代码function randomIndex() internal returns (uint) { uint totalSize = TOKEN_LIMIT - numTokens; uint index = uint(keccak256(abi.encodePacked(nonce, ms...

nftx闪电贷领取apecoin空投
已经是两个多月前的事件了,这几天才对这个tx进行了分析和fork重现,记录下来加深一下理解。apecoin的空投并没有限制调用方不能是合约地址,也不是用链下签名再到合约里验签的方式来领取空投,而是直接在合约里校验调用方的地址里有没有bayc/mayc,再加上apecoin当时的价格在8u左右,总体...

Uniswap V3交易预计算技巧
Uniswap上做swap交易时,比如用usdt购买btc,会根据界面上输入的usdt数量,实时计算出可以swap到多少个btc,v2版本因为是应用了xy=k的公式,可以方便的计算出来。代码里通过getAmountOut和getAmountIn得到,这两个都是view函数,不需要消耗gas。而到了...
Meebits mint随机算法
先上代码function randomIndex() internal returns (uint) { uint totalSize = TOKEN_LIMIT - numTokens; uint index = uint(keccak256(abi.encodePacked(nonce, ms...

nftx闪电贷领取apecoin空投
已经是两个多月前的事件了,这几天才对这个tx进行了分析和fork重现,记录下来加深一下理解。apecoin的空投并没有限制调用方不能是合约地址,也不是用链下签名再到合约里验签的方式来领取空投,而是直接在合约里校验调用方的地址里有没有bayc/mayc,再加上apecoin当时的价格在8u左右,总体...
Share Dialog
Share Dialog
Uniswap上做swap交易时,比如用usdt购买btc,会根据界面上输入的usdt数量,实时计算出可以swap到多少个btc,v2版本因为是应用了xy=k的公式,可以方便的计算出来。代码里通过getAmountOut和getAmountIn得到,这两个都是view函数,不需要消耗gas。而到了v3版本,不再是单独的xy=k公式,而是分割成一小块一小块的tick,swap交易时,划过一块一块的tick,最终才计算出可以换出多少个token,而v3也没有专门为了这个前端功能写一个view函数,而是用了callStatic调用,然后在合约里try…catch捕获revert信息并return来返回换得的数量。下面就解析一下这个奇淫技巧。
v3中有专门一个合约QuoterV2提供给前端调用
function quoteExactInputSingle(QuoteExactInputSingleParams memory params) public override returns ( uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate ) { bool zeroForOne = params.tokenIn < params.tokenOut; IUniswapV3Pool pool = getPool(params.tokenIn, params.tokenOut, params.fee); uint256 gasBefore = gasleft(); try pool.swap( address(this), // address(0) might cause issues with some tokens zeroForOne, params.amountIn.toInt256(), params.sqrtPriceLimitX96 == 0 ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) : params.sqrtPriceLimitX96, abi.encodePacked(params.tokenIn, params.fee, params.tokenOut) ) {} catch (bytes memory reason) { gasEstimate = gasBefore - gasleft(); return handleRevert(reason, pool, gasEstimate); } }
可以看到,上面调用了pool的swap方法去做swap交易,外面用try…catch捕获swap抛出的异常,再用handleRevert去解析reason信息。
在pool的swap方法里,有一段代码回调了QuoterV2的uniswapV3SwapCallback方法
// do the transfers and collect payment if (zeroForOne) { if (amount1 < 0) TransferHelper.safeTransfer(token1, recipient, uint256(-amount1)); uint256 balance0Before = balance0(); IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data); require(balance0Before.add(uint256(amount0)) <= balance0(), 'IIA'); } else { if (amount0 < 0) TransferHelper.safeTransfer(token0, recipient, uint256(-amount0)); uint256 balance1Before = balance1(); IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data); require(balance1Before.add(uint256(amount1)) <= balance1(), 'IIA'); }
再看下QuoterV2的uniswapV3SwapCallback方法
function uniswapV3SwapCallback( int256 amount0Delta, int256 amount1Delta, bytes memory path ) external view override { //省略其它代码 (bool isExactInput, uint256 amountToPay, uint256 amountReceived) = amount0Delta > 0 ? (tokenIn < tokenOut, uint256(amount0Delta), uint256(-amount1Delta)) : (tokenOut < tokenIn, uint256(amount1Delta), uint256(-amount0Delta)); IUniswapV3Pool pool = getPool(tokenIn, tokenOut, fee); (uint160 sqrtPriceX96After, int24 tickAfter, , , , , ) = pool.slot0(); if (isExactInput) { assembly { let ptr := mload(0x40) mstore(ptr, amountReceived) mstore(add(ptr, 0x20), sqrtPriceX96After) mstore(add(ptr, 0x40), tickAfter) revert(ptr, 96) } } else { // if the cache has been populated, ensure that the full output amount has been received if (amountOutCached != 0) require(amountReceived == amountOutCached); assembly { let ptr := mload(0x40) mstore(ptr, amountToPay) mstore(add(ptr, 0x20), sqrtPriceX96After) mstore(add(ptr, 0x40), tickAfter) revert(ptr, 96) } } }
这里用了opcode,mload读取下一个空闲指针地址,然后三个mstore把amountReceived,sqrtPriceX96After和tickAfter存入内存,并revert这个bytes。也就是说,这个回调函数把换得的token数量,换后的价格和最后价格所在的tick,这三个参数拼接成bytes,然后当做revert的信息,revert了这个交易。接着就被catch捕获了,然后用handleRevert去解析这个bytes。
function handleRevert( bytes memory reason, IUniswapV3Pool pool, uint256 gasEstimate ) private view returns ( uint256 amount, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 ) { //省略其它代码 (amount, sqrtPriceX96After, tickAfter) = parseRevertReason(reason); //省略其它代码 return (amount, sqrtPriceX96After, initializedTicksCrossed, gasEstimate); } function parseRevertReason(bytes memory reason) private pure returns ( uint256 amount, uint160 sqrtPriceX96After, int24 tickAfter ) { //省略其它代码 return abi.decode(reason, (uint256, uint160, int24)); }
这里直接用abi.decode解码了bytes,然后return给客户端。
而quoteExactInputSingle并不是view或者pure函数,调用还是会消耗gas的,这样岂不是达不到目的?其实,客户端用了callStatic方式来调用这个方法,也就是contract.callStatic.quoteExactInputSingle,它可以让节点伪装成不改变状态的调用去调用合约的方法,这样就不需要私钥也不会弹出钱包确认框。

Uniswap上做swap交易时,比如用usdt购买btc,会根据界面上输入的usdt数量,实时计算出可以swap到多少个btc,v2版本因为是应用了xy=k的公式,可以方便的计算出来。代码里通过getAmountOut和getAmountIn得到,这两个都是view函数,不需要消耗gas。而到了v3版本,不再是单独的xy=k公式,而是分割成一小块一小块的tick,swap交易时,划过一块一块的tick,最终才计算出可以换出多少个token,而v3也没有专门为了这个前端功能写一个view函数,而是用了callStatic调用,然后在合约里try…catch捕获revert信息并return来返回换得的数量。下面就解析一下这个奇淫技巧。
v3中有专门一个合约QuoterV2提供给前端调用
function quoteExactInputSingle(QuoteExactInputSingleParams memory params) public override returns ( uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate ) { bool zeroForOne = params.tokenIn < params.tokenOut; IUniswapV3Pool pool = getPool(params.tokenIn, params.tokenOut, params.fee); uint256 gasBefore = gasleft(); try pool.swap( address(this), // address(0) might cause issues with some tokens zeroForOne, params.amountIn.toInt256(), params.sqrtPriceLimitX96 == 0 ? (zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1) : params.sqrtPriceLimitX96, abi.encodePacked(params.tokenIn, params.fee, params.tokenOut) ) {} catch (bytes memory reason) { gasEstimate = gasBefore - gasleft(); return handleRevert(reason, pool, gasEstimate); } }
可以看到,上面调用了pool的swap方法去做swap交易,外面用try…catch捕获swap抛出的异常,再用handleRevert去解析reason信息。
在pool的swap方法里,有一段代码回调了QuoterV2的uniswapV3SwapCallback方法
// do the transfers and collect payment if (zeroForOne) { if (amount1 < 0) TransferHelper.safeTransfer(token1, recipient, uint256(-amount1)); uint256 balance0Before = balance0(); IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data); require(balance0Before.add(uint256(amount0)) <= balance0(), 'IIA'); } else { if (amount0 < 0) TransferHelper.safeTransfer(token0, recipient, uint256(-amount0)); uint256 balance1Before = balance1(); IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data); require(balance1Before.add(uint256(amount1)) <= balance1(), 'IIA'); }
再看下QuoterV2的uniswapV3SwapCallback方法
function uniswapV3SwapCallback( int256 amount0Delta, int256 amount1Delta, bytes memory path ) external view override { //省略其它代码 (bool isExactInput, uint256 amountToPay, uint256 amountReceived) = amount0Delta > 0 ? (tokenIn < tokenOut, uint256(amount0Delta), uint256(-amount1Delta)) : (tokenOut < tokenIn, uint256(amount1Delta), uint256(-amount0Delta)); IUniswapV3Pool pool = getPool(tokenIn, tokenOut, fee); (uint160 sqrtPriceX96After, int24 tickAfter, , , , , ) = pool.slot0(); if (isExactInput) { assembly { let ptr := mload(0x40) mstore(ptr, amountReceived) mstore(add(ptr, 0x20), sqrtPriceX96After) mstore(add(ptr, 0x40), tickAfter) revert(ptr, 96) } } else { // if the cache has been populated, ensure that the full output amount has been received if (amountOutCached != 0) require(amountReceived == amountOutCached); assembly { let ptr := mload(0x40) mstore(ptr, amountToPay) mstore(add(ptr, 0x20), sqrtPriceX96After) mstore(add(ptr, 0x40), tickAfter) revert(ptr, 96) } } }
这里用了opcode,mload读取下一个空闲指针地址,然后三个mstore把amountReceived,sqrtPriceX96After和tickAfter存入内存,并revert这个bytes。也就是说,这个回调函数把换得的token数量,换后的价格和最后价格所在的tick,这三个参数拼接成bytes,然后当做revert的信息,revert了这个交易。接着就被catch捕获了,然后用handleRevert去解析这个bytes。
function handleRevert( bytes memory reason, IUniswapV3Pool pool, uint256 gasEstimate ) private view returns ( uint256 amount, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 ) { //省略其它代码 (amount, sqrtPriceX96After, tickAfter) = parseRevertReason(reason); //省略其它代码 return (amount, sqrtPriceX96After, initializedTicksCrossed, gasEstimate); } function parseRevertReason(bytes memory reason) private pure returns ( uint256 amount, uint160 sqrtPriceX96After, int24 tickAfter ) { //省略其它代码 return abi.decode(reason, (uint256, uint160, int24)); }
这里直接用abi.decode解码了bytes,然后return给客户端。
而quoteExactInputSingle并不是view或者pure函数,调用还是会消耗gas的,这样岂不是达不到目的?其实,客户端用了callStatic方式来调用这个方法,也就是contract.callStatic.quoteExactInputSingle,它可以让节点伪装成不改变状态的调用去调用合约的方法,这样就不需要私钥也不会弹出钱包确认框。

No comments yet