-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.json
1 lines (1 loc) · 117 KB
/
search.json
1
[{"title":"AMM深入与解析","path":"/2024/07/22/AMM深入与解析/","content":"自动做市商(AMM)笔者在Uniswap-v1原理和源码分析 - Salbt’s blog中对AMM进行一些粗略的解释,以下我们将进一步阐述AMM在交易所中的应用和各种类型的衍生。 相关概念流动性(Liquidity):在AMM中的流动性是指将一种资产转化为另一种资产,这里资产可以是以太坊上的erc20代币 流动性池(Liquidity pools):交易的代币池,可以理解为存放两个代币储备金的金库。交易者可以自由在金库中进行代币的买卖。 流动性提供者(Liquidity Provider,LP):LP也可认为是做市商,LP可以根据池中代币权重添加储备金。他们通过向池子中交易收取交易费来获取利润。 去中心化交易所去中心化交易所(Decentralized Exchange,简称DEX)是一种无需中央机构进行控制和管理的加密货币交易平台。在去中心化交易所中,交易通过智能合约直接在区块链上执行,用户可以在没有中介的情况下进行点对点交易。 在当前公链上有两种方式实现的DEX: 1.基于订单薄的DEX CLOB模式(Central Limit Order Book,中心限价订单簿),是一个出价和报价组成的权限透明账本。能够了解交易参与者买卖价格。 卖家可以以指定价格出售加密货币,买家可以以指定价格购买加密货币。当前加密货币价格是买卖价格集中那个区间的值。 订单薄模式在链上运行有明显的缺点。对于像以太坊这样TPS较低的公链,很难满足订单薄高频的交易需求 这绝对是致命的缺点,有兴趣的小伙伴可以去了解下dydx,dydx最早就是在以太坊上使用订单薄模式的DEX。 上图是比特币2024.5.12 12:11左右的价格 2.基于流动性的DEX 将流动性池作为DEXs智能合约上的代币储备(reserve)。 AMM以一种算法的形式预测资产价格,链上的DEXs大多都式用AMM。 为DEX添加流动的LP可以得到DEX的LPT(Liquidity Provider Token, LPT)作为奖励,LP可以通过出售增值的LPT获取收益。 各种类型的AMM如今链上主要的DEX有:uniswap、curve、pancakeSawp、Raydium、Balancer。 想知道更多DEX可以在defillama中获取 现有流行的AMM类型数学函数如下: 1.恒定乘积做市商公式:x * y = k Uniswap、PancakeSwap、还有solana链上的Raydium都是恒定乘积的使用者,使用该算法的池子的流动性由两个代币的乘积获得。 当x供应增加,y的供应就会减少,从而导致流动性k维持在一个恒定的值不变。 当交易规模变大时,可能会出现巨大的滑点,我们将在稍后详细讲解滑点。 2.恒定和做市商公式:x + y = k 恒定和在图像上表现为一条直线,它是零滑点的理想模型(它的成交均价为k,即任何一处交易曲线上发生的交易的其斜率不变)。 对比恒定乘积公式,恒定和不能提供无限的流动性。套利者可以耗尽流动性池中全部的储备,流动性池也不能为其他交易者留下可用流动性。这种模式不适合大多数AMM使用。 3.恒定加权乘积做市商: Balancer是恒定加权乘积的使用者。恒定加权乘积是恒定乘积的一个变种,它允许在流动性池中添加多个代币,并使用该公式预测多个代币之间的价格。 它的优点是,交易者能够在池中进行任意资产的互换。 balancer的相关资料可以参考者篇文章:万字解析dYdX发展史:为何放弃L2,决意自建L1? | CoinVoice 4.稳定交换公式: 该公式由Curve推广,是恒定乘积与恒定和的混合体。 当用户资产组合相对平衡时,交易会发生在恒定和曲线上;当不平衡时,交易会切换到恒定乘积曲线上。 这个公式允许较低的滑点和无偿损失,但只适用于具有类似价值的资产,因为所需交易范围的价格总是接近于1。 AMM带来的风险AMM也不全部是优点,对于交易者和LP来说也有一定的风险: 滑点如何理解滑点(Price Slippage)? 我们可以通过Uniswap的AMM机制举例。我们能知道,Uniswap中两种token的价格会在一条曲线上变化,当交易订单越大(对于池子中代币总量来说很大)产生的滑点就越大,可从下图中看出: 假如我们拥有一个x*y = 4的流动新池。我们的初始价格为3.96 USD/ETH(图中橙色虚线),此时ETH储备大约为1 ,USD储备大约为3.96 。 我们想卖出大约3.28 USD/ETH个,可以从图中看出,这笔大交易使ETH价格从3.96降到了0.21 USD/ETH。 我们实际相当于支付的均价为:0.92 USD/ETH,这是我们初始价格的0.23倍左右。 上面我们用一个小流动性的池子和一个大交易,展示了滑点的产生过程。 在DEX中一般可以设置滑点容忍度,比如:我通过1 ETH换取2000 DAI,此时我能接受大约0.5%的滑点,那么我卖出1 ETH至少能获得2000 * 0.995 = 1990 DAI。 Front-Run和Back-Run接下来讲一讲抢跑交易(Front-Run)和尾随交易(Back-Run)。公链上的交易都是公开的,所有人都能够看到,所以任何都能够监视区块链上的交易。投机者(一般web3称他们为科学家)能够根据这些公开的交易在以太坊上进行操作从而牟利。 Front-Run:投机者在知道某个用户还未上链的交易后,会发起一个更高gas的交易。矿工会优先打包gas费高的交易,所以投机者的交易排序会先于这个用户的交易。 Back-Run:它的逻辑和Front-Run类似,投机者会某个用户还未上链的交易后发起一个较低gas的交易,矿工会将该交易在用户交易之后打包进区块。 你可能隐约知道了这些操作怎样去进行获利了,下面我通过一个Sandwich attack来具体分析Front-Run和Back-Run怎样去提取MEV(Maximal Extractable Value,最大可提取价值)。 三明治攻击(Sandwich attack)三明治攻击是什么? 三明治攻击和它的名字一样,攻击者会发送两笔交易将用户的交易夹在中间,通过Front-Run和Back-Run这两笔交易操作进行牟利。 ![sandwich attack](/images/posts/AMM/sandwich attack.jpg) 好了,现在我们将会在Uniswap上发起一次Sandwich attack。它会使用到Front-Run和Back-Run的逻辑提取MEV。 假设,我们有一个监视区块链的机器人,它会监视Uniswap中大交易。 机器人在监控的池子状态:eth-1000,usdt-3500000,liquidity-1000*3500000 此时有个交易者想要卖出20个eth,这个交易被我们的机器人检测到了。 于是我们发送两个交易:Front-Run和Back-Run Front-Run会抢先在交易者前,卖出5个eth。Back-Run在交易者后用卖出的eth得到的17412.936usdt去购买eth。 最终我们盈利了0.20046997eth 下面使用go简单的模拟上述过程,实际操作中需要考虑当前网络gas花费、各个流动池手续费差异、是否有竞争者这些因数。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364package mainimport (\t"fmt")type Pool struct {\tx, y float32\tk float32}func main() {\tp := NewPool(1000, 3500000)\tfmt.Println(p, "价格:", p.Price())\tvar cost float32 = 5\tgetUsdt := p.SwapXtoY(cost)\tfmt.Println("投机者获得usdt:", getUsdt, " 价格:", p.Price())\tp.SwapXtoY(20)\tfmt.Println(p, "价格:", p.Price())\tgetEth := p.SwapYtoX(getUsdt)\tfmt.Println("投机者净盈利eth: ", getEth-cost)}func NewPool(x, y float32) *Pool {\treturn &Pool{ x: x, y: y, k: x * y,\t}}func (p *Pool) SwapXtoY(dx float32) float32 {\tX := p.x\tY := p.y\tdy := dx * Y / (dx + X)\tp.x += dx\tp.y -= dy\tp.k = p.x * p.y\treturn dy}func (p *Pool) SwapYtoX(dy float32) float32 {\tX := p.x\tY := p.y\tdx := dy * X / (dy + Y)\tp.x -= dx\tp.y += dy\tp.k = p.x * p.y\treturn dx}func (p Pool) Price() float32 {\treturn p.y / p.x}func (p Pool) String() string {\treturn fmt.Sprint("池子当前状态: (tokenA: ", p.x, ", tokenB: ", p.y, ", liquidity: ", p.k, ")")} How to DeFi中展示了以太坊上的sandwich attack案例 无偿损失作为AMM中的LP,你可能会面临无偿损失。无常损失类似于衡量你在资金池中持有代币与在钱包中持有代币的机会成本。如果,你的代币还在流动性池中,那么无偿损失就还不会实现,只有当你移除流动性将代币拿出是才会实现。 我们可以继续用上面的代码来计算无偿损失。 假如,我们持有eth-10,usdt-35000,我们选择在uniswap中做市商。 然后有交易者在我们池子中卖出3个eth,此时价格从3500 滑倒了 2071 根据池子状态计算当前池子中总价值:53846 如果我们不做市商,那么我们持有代币时总价值应该为:55710 1234567891011121314151617181920212223242526func main() {\tholdx := float32(10)\tholdy := float32(35000)\tp := NewPool(holdx, holdy)\tfmt.Println(p, "价格:", p.Price())\t// 池子当前状态: (tokenA: 10, tokenB: 35000, liquidity: 350000) 价格: 3500 p.SwapXtoY(3)\tfmt.Println(p, "价格:", p.Price())\t// 池子当前状态: (tokenA: 13, tokenB: 26923.078, liquidity: 350000) 价格: 2071.006\tpoolV := p.Price()*p.x + p.y\tfmt.Println("池中总价值:", poolV) // 池中总价值: 53846.156\tholdV := holdx*p.Price() + holdy\tfmt.Println("如果持有代币总价值:", holdV) // 如果持有代币总价值: 55710.062\tlostV := holdV - poolV\tfmt.Println("损失价值:", lostV, "百分比", lostV/holdV*100, "%") // 损失价值: 1863.9062 百分比 3.3457265 %} 计算无偿损失的公式 可以用go检验一下这个公式 1234d := 2071.006 / 3500IL := 2*math.Sqrt(d)/(d+1) - 1fmt.Println(IL)// -0.03345724411216 参考资料How to DeFi","tags":["以太坊","区块链","DeFi"],"categories":["区块链","DeFi"]},{"title":"Uniswap-v2(二)-源码分析","path":"/2024/07/22/Uniswap-v2-二-源码分析/","content":"Uniswap-v2架构Uniswap v2的代码结构采用了Core-Periphery,这种架构将交易对的数据状态和底层函数这类上链就不能更改的部分归入Uniswap Core,而将非核心功能的代码放入Uniswap Periphery。 为什么要使用Core-Periphery代码结构在研究Uniswap v2源码时,我对它的代码结构产生了很大的疑惑,为什么要使用Core-Periphery代码结构。研究v2代码之前,我也对Maker协议的源码进行过分析,它们的合约代码有一个特点:底层逻辑和上层业务是分离的。 为什么它们合约结构会有这个特点? 我们可以从区块链的特性解释。我们知道一旦合约上链,其代码内容就不可篡改。如果我们将代码的所有逻辑写入一个合约中,那么我们在合约上链之后就不能对业务功能进行升级了。而我们可以讲合约代码分为多个逻辑,比如一部分是不可修改的底层逻辑代码,另一部分是建立底层逻辑上可以更换的。 在v2-periphery中我们也能够发现Core-Periphery代码结构带来的好处,仓库中有两个UniswapV2Router合约,其中Router02是Router01的改进版。 Core和Periphery包含的代码了解Core-Periphery代码结构后,我们分析下Core和Periphery合约构成。Core和Periphery将swap、流动性管理分成了两部分,Core合约包括了合约的底层逻辑,这里的底层逻辑是向用户Tranfer代币。而Periphery的逻辑是用户向交易池Transfer代币。 Core Core将swap的卖token的过程,即发送token到交易者地址的功能放在UniswapV2Pair,这过程还会检查流通性k的合法性(knew >= kpervious)。此外添加流通性和移除流动性需要使用的mint和burn也在该合约实现。 UniswapV2ERC20 UniswapV2Factory UniswapV2Pair Periphery Periphery将swap的买token的过程,即发送token到pair地址的功能放在UniswapV2Router02。添加流通性和移除流动性之前需要transfer的token也会在该部分实现。 UniswapV2Router01 UniswapV2Router02 UniswapV2Migrator Uniswap-v2 CoreUniswap-v2 core是一个Uniswap核心部分,其UniswapV2ERC20是uniswap项目的LPT,每当用户添加流动性就能够mint一笔LPT,同样用户也可以burn一笔LPT移除流动性。 流动性管理和交易由UniswapV2Pair负责,swap的计算和流动性相关计算笔者在Uniswap-v1介绍和源码分析 - Salbt’s blog中进行了讲解。UniswapV2Pair实现了TWAP和闪电贷,我们会在下面深入。 然后v1中还有一个工厂合约,在v2中对工厂进行了升级,v2使用ceate2创建合约能计算出UniswapV2Pair具体地址。在UniswapV2Factory会深入讲解create2这个指令。 UniswapV2ERC20其中UniswapV2ERC20作为项目的LPT,它是一个ERC20标准的Token,当用户添加流动性时就能获得UniswapV2ERC20代币(Uni)。 这里展现UniswapV2ERC20的元数据,ERC20标准不作过多介绍,详细查看:ERC-20: Token Standard (ethereum.org) 12345678contract UniswapV2ERC20 { using SafeMath for uint; string public constant name = 'Uniswap V2'; string public constant symbol = 'UNI-V2'; uint8 public constant decimals = 18; // ...} UniswapV2FactoryUniswapV2Factor功能大致能分为两个:打开手续费开关和创建pair。打开手续费通过setFeeTo(address _feeTo)函数实现,如果地址没有指定,交易池就不会收取协议费用。 这里我们重点分析UniswapV2Factory的createPair函数。 v2的createPair允许添加两个token地址。 v2使用create2创建合约,等下会介绍create2创建合约的优势。 v2会将ERC20 pair存储在合约中,不同ERC20之间的交易可以通过路由找寻。 123456789101112131415161718192021function createPair(address tokenA, address tokenB) external returns (address pair) { require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES'); // 对token进行排序,这样就能支持任意的无序token pair创建 (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS'); // 不允许已存在的pair创建 require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // 创建合约 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); // 存储ERC20 pair到工厂中 getPair[token0][token1] = pair; getPair[token1][token0] = pair; // populate mapping in the reverse direction allPairs.push(pair); // 触发事件 emit PairCreated(token0, token1, pair, allPairs.length);} create2上面介绍了,Uniswap工厂合约使用内联汇编中的create2创建合约,下面就详细介绍create2的作用。 create2(value, offset, size, salt):create2和create功能相同,能够从内存中指定offset提供的初始化代码创建新合约。但是create2部署的合约会使用salt能得到一个确定的合约地址。 value:向创建合约发送的ETH,单位为wei。 offset:内存中的字节偏移量,以字节为单位,新帐户的初始化代码。。 size:要复制代码的大小。 salt:用于在确定地址上创建新帐户的32字节值。 12345bytes memory bytecode = type(UniswapV2Pair).creationCode;bytes32 salt = keccak256(abi.encodePacked(token0, token1));assembly { pair := create2(0, add(bytecode, 32), mload(bytecode), salt)} 解释上面的代码: 通过type(UniswapV2Pair).creationCode获得包含创建合约字节码的内存字节数组。 用输入的token pair参数作为salt,并使用abi.encodePacked进行编码。 使用create2创建合约。 用create2创建一个合约(工厂模式)工厂代码 1234567891011121314151617181920// SPDX-License-Identifier: MITpragma solidity ^0.8.0; import "./Part.sol";interface IPart { function initialize(uint, uint) external;}contract Factory { function create2Test(uint _type, uint _szie) external returns (address part) { bytes memory bytecode = type(Part).creationCode; bytes32 salt = keccak256(abi.encodePacked(_type,_szie)); assembly { part := create2(0, add(bytecode, 32), mload(bytecode), salt) } IPart(part).initialize(_type, _szie); }} 产品代码 1234567891011121314// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Part { uint public Type; uint public Szie; function initialize(uint _type, uint _szie) external { Type =_type; Szie = _szie; }} 现在我们研究一下create2代码在EVM是怎样工作从而来理解create2。代码部署之后,我们使用create2Test部署part合约,进入remix调试模式。 opcode跳到create2附近 此时我们观察EVM栈和内存的状态,内存中地址0x80是存放bytecode(part合约)的长度值,我们能够知道合约大小为0x1a7,合约代码存储内存中0xc0~0x240。 使用调试器进入到0186步,这时栈会执行cretae2指令。我们继续观察栈的状态,此时栈压入了四个值,分别是:0x00,0xa0,0x1a7,0xcc69885fda6bcc1a4ace058b4a62bf5e179ea78fd58a1ccd71c22cc9b688792f 我们分析一下这几个值是如何计算的: 首先0xcc69…792f是两个参数的keccak256得到的,在create2参数入栈时,这个hash值就计算在栈顶,通过DUP将其复制在栈顶 然后栈读到MLOAD命令,将bytecode在内存中的偏移位置(offset)0x80中的值0x1a7加载到栈顶。 之后使用ADD命令,将bytecode在内存中的偏移位置(offset)0x80加上32 byte,获得的值是bytecode字节码开始的位置0xa0。我们可以简单的验证一下,使用0xa0+0x1a7(合约代码起始位置+合约长度)能计算出合约字节结束在内存中0x240位置 注:该部分需要掌握solidity储存布局和EVM指令集相关知识。可以参考笔者以下文章:合约安全(一)-数据存储 - Salbt’s blog、深入EVM(二)-EVM指令集 - Salbt’s blog UniswapV2PairUniswapV2Pair合约继承了UniswapV2ERC20,它在pair中会跟踪两个ERC20的储备金数量,并用blockTimestampLast记录储备金更新的时间,可以使用getReserves()函数获取这三个值。实现了两个TWAP的累加器。 reserve0,reserve1:两中Token储备金数量。 blockTimestampLast:上一次储备金更新的时间。 price0CumulativeLast,price1CumulativeLast:TWAP累计器 kLast:流动性,kLast=reserve0 * reserve1 1234567891011contract UniswapV2Pair is UniswapV2ERC20 { uint112 private reserve0; // uses single storage slot, accessible via getReserves uint112 private reserve1; // uses single storage slot, accessible via getReserves uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves uint public price0CumulativeLast; uint public price1CumulativeLast;\tuint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity} UniswapV2Pair功能就是向Periphery提供修改上面参数功能。在Uniswap进行业务时,会使用swap买卖token,需要更新池中reserves的值并更新TWAP。使用addLiquidity会增加流动性并mint LPT、使用removeLiquidity会减少流动性并burn LPT 更新Reserver和TWAP_update首先会更新TWAP累加器: dT = blockTimestamp(当前时间) - blockTimestampLast(上一次更新时间)。 price Acc = price0CumulativeLast + (_reserve1/ _reserve0)*dT 然后根据输入的新的token的储备金balance更新reserve。 reserve = uint112(balance) 123456789101112131415// update reserves and, on the first call per block, price accumulators function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, '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); } 具有闪电贷的SwapUniswapV2Pair包含了最低层次的Swap,这个swap负责将卖出的token通过safeTransfer发送给to地址。如果传入的参数包含data部分,swap会进入一个回调函数uniswapV2Call触发flash swap。具体代码过程如下: 检查输出token是否合法,不能通过有两个amount0Out > 0。 获取当前两个token的储备金,并检查amountOut是否超过reserve。 检查to的地址是否合法,to地址不能为token地址。 转账 如果amountOut>0就向to转账。 如果data存在,转账之后会进入到uniswapV2Call函数触发flash swap。 计算新流动性knew >= kpervious 其中knew = (xnew - dx * 0.03) * ynew kpervious = xpervious * ypervious 使用_update更新 12345678910111213141516171819202122232425262728293031// 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 // callflash swap 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); } 使用flash swap要实现IUniswapV2Callee接口,可以参考代码:Tutorials/Flash_swap/contracts/flashswap.sol at main · UV-Labs/Tutorials (github.com) 手续费如果开启手续费,Uniswap会在mint和brun时收取协议费用。合约的_mintFee函数会根据收取1/6的LPT增值部分,通过增发LPT的方式分走这部分利润。 在UniswapV2Factor能够指定协议费用的收取地址。 1234567891011121314151617181920// if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k) function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) { address feeTo = IUniswapV2Factory(factory).feeTo(); feeOn = feeTo != address(0); uint _kLast = kLast; // gas savings if (feeOn) { if (_kLast != 0) { uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1)); uint rootKLast = Math.sqrt(_kLast); if (rootK > rootKLast) { uint numerator = totalSupply.mul(rootK.sub(rootKLast)); uint denominator = rootK.mul(5).add(rootKLast); uint liquidity = numerator / denominator; if (liquidity > 0) _mint(feeTo, liquidity); } } } else if (_kLast != 0) { kLast = 0; } } Uniswap-v2 PeripheryUniswap-v2 Periphery有两个函数:UniswapV2Migrator、UniswapV2Router02(UniswapV2Router01是02的旧版本)。 UniswapV2Migrator能够将v1池子迁移到v2的合约,这里不过多介绍,可以直接去了解源码:v2-periphery/contracts/UniswapV2Migrator.sol at master · Uniswap/v2-periphery (github.com) UniswapV2Router02UniswapV2Router02实现了uniswap的业务代码,比如:addLiquidity、addLiquidityETH、removeLiquidity、removeLiquidityETH、swapExactTokensForTokens和swapTokensForExactTokens等方法。 我们讲到UniswapV2Pair中的swap是最底层的功能函数,负责将买到的token发送给交易者。而UniswapV2Router02提供的swap会先将卖出的token发送给Pair。 1234567891011121314function 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); } 我们可以从其中一个swap功能看到,在调用_swap前会先使用TransferHelper.safeTransferFrom发送卖出的token。 寻找任意pair之间的路径下面分析Periphery如何进行任意pair对之间的交易。 使用for对path进行遍历。 每次遍历获得前一个token和后一个token的pair。 在pair中进行交换: 如果没有遍历完,交换得到的token应该发送给下一个pair,回到开始继续。 如果遍历完,将得到的token发送给to地址。 1234567891011121314// **** SWAP **** // requires the initial amount to have already been sent to the first pair 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) ); } } 图例: UniswapV2Router02相比于UniswapV2Router01提供swap的限价订单,基于_swapSupportingFeeOnTransferTokens底层函数实现swapExactTokensForTokensSupportingFeeOnTransferTokens、swapExactETHForTokensSupportingFeeOnTransferTokens等。 Uniswap工具库Uniswap v2使用了多个数学库,比如FixedPoint、Math、SafeMath和UQ112x112,感兴趣的可以去看一下源码。我们需要关注的是UniswapV2Library和UniswapV2OracleLibrary。 UniswapV2Library提供都是无需gas费的方法,能够查询pair地址、pair之间价格、获得pair储备金。 UniswapV2OracleLibrary提供查看TWAP的方法。 注:使用pairFor生成地址时,需要更改其中的硬编码部分。计算pair地址部分使用了c779f884b0d3b96c99d18260ba7f1b2c9a66dcddcacbcdf30f304d308cd4976e。需要使用keccak256(type(UniswapV2Pair).creationCode)重新计算init code hash Periphery计算pair地址也是通过这种方式得到的,如果不更改,合约添加流动时就会报错。 重新计算 init code hash 123function pairCodeHash() external pure returns (bytes32) {\treturn keccak256(type(UniswapV2Pair).creationCode);} 参考 Uniswap Labs (github.com) https://github.com/Jeiwan/uniswapv2-contracts Flash Swaps! Learn how to use Uniswap’s Flashswap feature on the mainnet | BuildBear Labs (medium.com)","tags":["以太坊","DeFi","uniswap","源码分析"],"categories":["ethereum","DeFi","Uniswap"]},{"title":"Uniswap-v2(一)-功能和原理分析","path":"/2024/07/17/Uniswap-v2(一)功能和原理分析/","content":"Uniswap-v2介绍uniswap-v1在以太坊链上基于”constant product formula”(恒定乘积公式)实现了AMM。v1能为ETH和ERC20提供流动性,并从中收取一定的交易费用于支付给流动性提供者(LP,Liquidity Provider)。而Uniswap v2是基于v1的原理上重构代码,并新增许多实用的功能。接下将会介绍v2中新增的功能和它们的原理。 新增功能。uniswap-v2是基于v1新增了许多功能,下面介绍v2主要新增的功能: 允许任意erc20/erc20 pair flash swap 使用时间加权平均估算价格 协议费用开关 支持任意的ERC20 pair与v1不同,v2支持了任意 ERC20/ERC20 pair之间交易。为了理解v2是怎样优化v1这部分功能,我们用uniswap白皮书中的例子进行讲解。 对于一个 ABC 和 XYZ的交易池。 v1:如果使用v1协议创建这两个ERC20 pair,需要创建ABC/ETH,XYZ/ETH两个交易池。其中ABC/XYZ需要经过ETH作为桥接货币。 因为v1的代码决定了它只能创建ERC20和ETH的交易池。 v2:支持了创建任意两个ERC20 pair,能够直接创建 ABC/XYZ 的交易池。不需要经过ETH桥接。 v2的这项功能优点: 解决了v1的ETH/ABC pair和 ETH/XYZ pair这种交易对带来的碎片化。 v1的LP需要提供两倍于v2的LP的资产才能创建相同资产数量的ABC/XYZ pair。 对于LP来说,使用v1添加 ABC/XYZ pair还需要提供ETH作为这两者桥接货币,那么LP也会面临ETH与ABC、XYZ的无偿损失。而v2的LP直接需要面对ABC与XYZ的无偿损失。 假设 ABC、XYZ是稳定币,v1的无偿损失会比v2的无偿损失更大。下图无偿损失例子计算: 闪电贷(Flash Swap)闪电贷(Flash Swap):闪电贷是区块链一个特殊的机制。用户被允许在一个区块内自由借出资金,并在这个区块归还资金和其费用,整个借贷过程无需任何抵押品。 为什么会有闪电贷? 区块链的原子性:我们知道比特币、以太坊等区块链只有在区块上链时区块链状态才会发生改变。这就意味着想要通过一系列交易改变某个状态需要这些交易都能成功并被打包上链,这就是区块链的原子性。 没有归还贷款可以revert:在区块链里我们可以检查一个区块里借出的贷款是否在这个区块内被归还,这是由合约逻辑决定的。如果这个区块内没有归还贷款,合约就会revert交易。 区块链的原子性导致了闪电贷的出现:闪电贷是无需任何抵押品的,这是因为用户没有在区块内归还贷款,那么发出借贷的交易永远不可能上链,该用户的资产状态也不可能有任何变化。 闪电贷有什么应用 套利:比如现在有两个类似uniswap的交易所,其中一个以太坊价格为3500$,另一个为3700$。那么套利者可以使用闪电贷在3500$的交易所买入一个ETH,然后在另一个交易所以3700$的价格出售。整个过程套利者只需支付闪电贷一点点手续费就能获得200$的利润。 再融资贷款:一些常规的借贷DeFi会需要抵押品,比如1eth能抵押出2000DAI。当用户借出的DAI用于其他项目时,用户可以使用闪电贷赎回自己eth,并用部分eth偿还闪电贷。 偿还即将清算的抵押品:像Maker这类协议,用户抵押品清算时会受到一笔清算惩罚,抵押品会用于归还债务和扣除10%的惩罚,剩下返回给用户。为了避免清算惩罚,可以使用闪电贷赎回抵押品,然后用抵押品归还闪电贷,这样就能规避清算惩罚。 ERC3156-闪电贷实现标准flash Swap使用ERC3156标准实现。ERC3156有两个合约接口需要实现,分别是FlashBorrower和FlashLender。 借出方需要实现接口: 1234567891011121314151617181920212223242526272829303132333435363738394041// SPDX-License-Identifier: MITpragma solidity >=0.6.0 <0.9.0;import "./IERC3156FlashBorrower.sol";interface IERC3156FlashLender { /** * @dev The amount of currency available to be lended. * @param token The loan currency. * @return The amount of `token` that can be borrowed. */ function maxFlashLoan( address token ) external view returns (uint256); /** * @dev The fee to be charged for a given loan. * @param token The loan currency. * @param amount The amount of tokens lent. * @return The amount of `token` to be charged for the loan, on top of the returned principal. */ function flashFee( address token, uint256 amount ) external view returns (uint256); /** * @dev Initiate a flash loan. * @param receiver The receiver of the tokens in the loan, and the receiver of the callback. * @param token The loan currency. * @param amount The amount of tokens lent. * @param data Arbitrary data structure, intended to contain user-defined parameters. */ function flashLoan( IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data ) external returns (bool);} 贷款方需要实现接口: 1234567891011121314151617181920212223// SPDX-License-Identifier: MITpragma solidity >=0.6.0 <0.9.0;interface IERC3156FlashBorrower { /** * @dev Receive a flash loan. * @param initiator The initiator of the loan. * @param token The loan currency. * @param amount The amount of tokens lent. * @param fee The additional amount of tokens to repay. * @param data Arbitrary data structure, intended to contain user-defined parameters. * @return The keccak256 hash of "ERC3156FlashBorrower.onFlashLoan" */ function onFlashLoan( address initiator, address token, uint256 amount, uint256 fee, bytes calldata data ) external returns (bytes32);} 实现代码可以参考:https://github.com/alcueca/ERC3156/tree/main 时间加权平均价格预言机价格预言机是向区块链提供各种加密货币价格的信息输入机。Uniswap作为一个去中心化交易所,它也具备有提供价格信息的能力。 Uniswap v1的价格是交易池中两种货币的比率(ratio),这种瞬时的价格是及其容易被操控的。比如:攻击者可以通过使用闪电贷使价格发生剧烈变化,然后对使用uniswap预言机价格进行决策的合约进行攻击。 Uniswap v2使用TWAP作为价格预言机的机制,它的大致思路是: 价格取决于过去一段时间价格的平均值。 当前的价格不会被计算到oracle中。 TWAP时间加权平均价格(Time Weighted Average Price):Uniswap v2使用一个时间加权的价格累加器作为快照,然后用这个累加器除以累加器经过的时间得到时间加权平均价格。 注明:Ti是价格Pi所处的一段时间,ti是价格Pi所处的时刻 当我们对现在,假设是t4到之后t7的价格感兴趣,我们可以记录当下的累加器快照和t7时刻累加器的快照。计算方式如下: (P4*T4+P5*T5+P6*T6+P7*T7)/(T4+T5+T6+T7) 即 (a7-a4)/(t4-t1) TWAP需要解决的问题跟踪两个资产TWAP如果你尝试计算 ETH/DAI 和 DAI/ETH 的TWAP,你会发现它们价格并不是呈倒数关系。比如:ETH/DAI 的价格从100涨到300,时间为1,计算出TWAP(ETH/DAI) = 200。而如果使用 DAI/ETH 进行计价,那么价格是从1/100 跌到 1/300,计算出TWAP(DAI/ETH) = 1/150。可以看出 TWAP(ETH/DAI) 与 TWAP(DAI/ETH)不是倒数关系。所以Uniswap需要跟踪两个资产的TWAP。 及时更新价格在uniswap v2代码的时间累加器的更新由_updata()函数触发。这会导致以下两种情况出现: 攻击者绕过更新累加器去更改池子的余额和价格 一段时间没有触发swap这类函数去更新累加器 这两种情况都会使得TWAP不能及时更新,为了避免这种情况,合约提供了 sync() 函数能够主动触发_updata()更新TWAP。 协议费用(Protocol fee)uniswap v2设置了一个协议费用(Protocol fee),如果开启这个协议费用,每次LP添加流动性时就会为feeToSetter的地址mint一笔LPT作为协议收取的费用。增发LPT分走LP部分利润的公式如下: 接下将通过三步逐渐分析,Uniswap v2是怎样通过上面的公式收取协议费用 LPT增值通过swap的手续费,池中储备x*y会逐渐变大,LP通过添加流动性mint的LPT也会增值。LP可以通过burn LPT获得的代币也会更多。 增发LPT拿走LPT增值的利润项目方可以通过增发LPT的方式拿走池子中部分收益。下面假设协议通过增发LPT拿走LPT增值所有利润。此时LP手中持有的LPT增值为0,因为增值部分价值通过增发LPT分走了。 实际上协议费只会分走一定比例的增值利润,协议费用的公式推导如下: Uniswap v2设置的φ = 1/6,即协议费用等于LP赚取1/6 的利润。交易中为交易支付费用0.30%,LP可以获得池子交易费用的0.25%,协议获得0.05%。 引用 Uniswap V2 Book | RareSkills whitepaper.pdf (uniswap.org) Jeiwan/uniswapv2-contracts: Uniswap V2 contracts, ready to be deployed to local or test network (github.com) 梁培利的个人空间-梁培利个人主页-哔哩哔哩视频 (bilibili.com)","tags":["以太坊","uniswap","defi"],"categories":["ethereum","DeFi","Uniswap"]},{"title":"计算机网络应用层协议-HTTP","path":"/2024/07/15/HTTP协议/","content":"HTTPHTTP(HyperText Transfer Protocol,HTTP)是实现Web的应用层协议,HTPP由两个程序实现:一个客户端程序和服务器程序。这两个程序运行在两个设备上,使用HTTP报文进行会话。在深入HTTP之前,先解释一些Web的术语。 Web相关概念 Web页面(Web page):Web页面是由对象组成的。一个对象(object)是一个文件,这个文件可以是HTML、JPRG图形、一个JAVA程序或一段视频这样的文件。 多数Web页面会包含一个HTML基本文件以及引用的几个对象 Web浏览器(Web browser):Web浏览器是实现了HTTP协议的客户端(client)。 Web服务器(Web server):Web服务器是实现了HTTP协议的服务器,能够存储Web对象,每个对象由URL寻址获得。 HTTP概况HTTP定义了Web浏览器向Web服务器请求Web页面的方式、服务器向Web浏览器发送页面的方式。基本的思路如下图: HTTP使用TCP作为运输层协议,在服务器和客户端建立起TCP连接后,浏览器和客户端进程就能通过套接字(socket)访问TCP。 Web服务器能够从套接字获得从Web客户端发送的HTTP请求报文,同样Web客户端能从套接字获得从Web服务器发送的HTTP响应报文。当HTTP报文通过套接字进入TCP,报文就会脱离程序的控制并进入TCP的控制。这就是划分网络协议层的原因,我们无需关注TCP的工作,只需要关注应用层(HTTP)怎样处理报文。 无状态协议HTTP是一个无状态协议,因为HTTP服务器不会保存用户的任何信息。但这不代表Web服务器不会保存任何信息,HTTP提供了cookie以支持Web服务器跟踪用户内容。 用go创建一个Web服务器和客户端在浏览器或者使用curl命令访问本地 http://127.0.0.1:8080 ,会返回一个”welcome to this Web Page” 1234567891011121314151617package mainimport (\t"fmt"\t"net/http")func main() {\thttp.HandleFunc("/", myHandle)\thttp.ListenAndServe("127.0.0.1:8080", nil)}func myHandle(w http.ResponseWriter, r *http.Request) {\t// 回复\tw.Write([]byte("welcome to this Web Page"))} 或者编写一个客户端发送HTTP请求 123456789101112131415161718192021222324252627package mainimport (\t"fmt"\t"io"\t"net/http")func main() {\tresp, _ := http.Get("http://127.0.0.1:8080")\tdefer resp.Body.Close()\tbuf := make([]byte, 1024)\tfor { // 接收服务端信息 n, err := resp.Body.Read(buf) if err != nil && err != io.EOF { fmt.Println(err) return } else { fmt.Println("读取完毕") res := string(buf[:n]) fmt.Println(res) break }\t}} 代码来源:http编程 · Go语言中文文档 (topgoer.com) 非持续连接和持续连接在客户端和服务器进行通信前,必须先建立起连接,然后客户端就能够发送一系列请求。客户端-服务器的连接是通过TCP建立起的,应用程序的设计者就需要考虑一个问题,即每个请求/响应对是经过一个TCP连接发送,还是所有请求/响应对都经过一个TCP连接发送。 这两种方式分别称为: 非持续连接:每次发送一个HTTP请求都需要建立一个TCP连接,在客户端收到HTTP响应后关闭TCP连接。 持续连接:发送一系列HTTP请求可以在一个TCP连接得到响应,服务器会在一段时间后关闭TCP连接。 下图中展示了三种HTTP连接的方式,也是HTTP模型发展的三个阶段:HTTP/1.0、HTTP/1.1、HTTP/2.0。前两者是我们介绍的非持续连接和持续连接的方式实现的,第三个采用了流水线的持续连接设计思路允许在同一TCP连接中并发的处理HTTP请求和回复HTTP响应。 Source: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Connection_management_in_HTTP_1.x 可以看出这三种方式的优劣: HTTP/1.0会频繁建立TCP连接,这会给服务器带来严重的负担。HTTP/1.1改善了这个问题,它使TCP连接保持打开,等待一个时间间隔(没有被使用)后关闭连接。HTTP/2.0能够比前两者减少更多的延迟,但不是所用请求都适合使用这种方式,这里不作过多介绍。 HTTP报文格式HTTP报文有两种:请求报文和响应报文、 HTTP请求报文HTTP请求报文主要由三部分组成: 请求行(request line):包含三个字段 方法字段:HTTP常见的方法包括GET、POST、HEAD、PUT和DELETE URL:客户端请求的对象 HTTP版本:一般客户端默认版本使HTTP/1.1 首部行(header line):首部行是向服务器或客户端提供一些必要的信息,比如:Host指明对象所在的主机,Connect指明发送完该对象后是否关闭(Keep-Alive/close),User-agent指明用户代理(向用户发送请求的浏览器类型)。 实体体(entity body):实体体可以看作报文运输的货物。一般使用Post方法的报文会携带数据,这个数据可能来自网页的输入。 下面展示一个HTTP请求报文,第一行是请求行,其中使用方法是GET,请求对象 /,之后三行是首部行。GET方法一般没有实体体。 1234GET / HTTP/1.1 Host: localhost:8080User-Agent: Mozilla/5.0Connection: keep-alive 可以在浏览器使用F12打开开发者工具查看网络中的HTTP请求,Edge、Google等主流浏览器都支持。 HTTP响应报文HTTP响应报文与HTTP请求相似,但第一行与它不同,我们分析HTTP响应报文的状态行: 状态行分为三部分: 版本:服务器使用HTTP版本,一般是HTTP/1.1 状态码和短语:状态码是表明特定HTTP是否完成成功,短语与状态码相对应。常见响应如下 200 OK:请求成功,信息返回响应报文中 301 Move Permanently:请求的对象被永久转移,新的URL定义在响应报文首部行的 Localtion。客户软件会自动获取新的URL 400 Bad Requset:一个通用差错代码,指示该请求不能被服务器理解 下面展示一个HTTP响应报文,响应报文也能在浏览器中查看。 12345HTTP/1.1 200 OKContent-Length:24Content-Type:text/plain; charset=utf-8Connection: keep-aliveDate:Fri, 19 Jul 2024 10:15:02 GMT Cookie-解决HTTP无状态的设计前面我们提到HTTP本身是一个无状态的协议,这能够简化服务器的设计,并且允许工程师去开发同时能处理上千个。 假如一个网站希望能够识别用户,可能是服务器用于限制用户的访问,或者是因为它想将内容与用户联系起来。这个时候,我们就能够使用cookie对用户进行跟踪,目前大多数Web站点都是用了cookie技术。 cookie实现需要实现四个组件 HTTP请求报文的首部行有cookie HTTP响应报文的首部行有cookie 用户端系统中有一个cookie文件,一般由用户的浏览器进行管理 Web站点有一个后端数据库 cookie工作过程 PC端访问浏览器的Web对象,请求报文到达Web服务器后,服务器会创建一个识别码,以此作为索引在它的后端数据库中产生的表项。 Web服务器会在HTTP响应报文的首部行添加Set-cookie:识别码 ,PC端接收后会根据Set-cookie在其特定的cookie文件中添加一行。 PC之后向该服务器继续访问Web对象时会在HTTP请求报文首部行添加cookie字段,服务器可以根据请求报文中cookie跟踪用户动作或完成一些动作。 PC端在一周后继续访问,加入服务器的cookie没有过期,服务器能够继续使用该cookie跟踪用户。 go代码模拟我们使用go实现上述流程,我们假设一个场景: 用户Alice是这个网站注册用户,首先Alice通过index网站登录,此时服务器发送HTTP响应报文时会携带一个Set-cookie发送给她的PC浏览器。 然后Alice要在这个网站下获得一些需要的信息,Alice向该网站的另一个资源发出请求,发送的HTTP请求报文携带有cookie。 网站服务器通过cookie确定Alice的身份,然后根据Alice在数据库中的信息推荐给她需要的信息。 服务器代码 123456789101112131415161718192021222324252627282930313233343536373839404142434445package mainimport (\t"net/http")// 模拟后端数据库var db map[string]stringfunc main() {\tRUnDB()\thttp.HandleFunc("/", HandleIndex)\thttp.HandleFunc("/HandleCookie", HandleCookie)\thttp.ListenAndServe("127.0.0.1:8080", nil)}func RUnDB() {\tdb = make(map[string]string)\t// 假设alice已经注册过了\tdb["username"] = "Alice"}func HandleIndex(writer http.ResponseWriter, request *http.Request) {\t// 为客户端设置一个cookie id\tcookie := http.Cookie{ Name: "username", Value: "Alice",\t}\t// 响应报文添加set-cookie\thttp.SetCookie(writer, &cookie)\twriter.Write([]byte("HandleIndex Page: 使用cookie收集了你的信息"))}func HandleCookie(writer http.ResponseWriter, request *http.Request) {\t// 接受客户端传来的cookie\tcookie := request.Cookies()[0]\t// 使用cookie完成动作\tif cookie.Value == db[cookie.Name] { writer.Write([]byte("HandleCookie Page: 根据用户推荐相关信息"))\t}} 客户端 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475package mainimport (\t"fmt"\t"io"\t"net/http"\t"strings"\t"time")// 模拟存放cookie的管理器var db map[string]string = make(map[string]string)func main() {\tRunDB()\tGet("http://127.0.0.1:8080", "/", false)\tGet("http://127.0.0.1:8080", "/HandleCookie", true)\ttime.Sleep(7 * time.Second)\tfmt.Println("模拟一个周后")\tGet("http://127.0.0.1:8080", "/HandleCookie", true)}func RunDB() {\tdb = make(map[string]string)}func Get(url string, path string, hasCookie bool) {\t// 创建一个http.Client对象\tclient := &http.Client{}\t// 创建一个http.Request对象\treq, _ := http.NewRequest("GET", url+path, nil) // 发送cookie\tif hasCookie { // 从数据库中获取键值 s := strings.Split(db[url], "=") // 请求中添加cookie cookie := http.Cookie{ Name: s[0], Value: s[1], } req.AddCookie(&cookie)\t} // 3. 发送请求\tresp, _ := client.Do(req)\tdefer resp.Body.Close() // 请求中有Set-Cookie,则将Set-Cookie保存到数据库中\tif resp.Header.Get("Set-Cookie") != "" { db[url] = resp.Header.Get("Set-Cookie") fmt.Println("Set-Cookie:", resp.Header.Get("Set-Cookie"))\t}\tOutput(resp)}func Output(resp *http.Response) {\tbuf := make([]byte, 1024)\tfor { // 接收服务端信息 n, err := resp.Body.Read(buf) if err != nil && err != io.EOF { fmt.Println(err) return } else { res := string(buf[:n]) fmt.Println(res) break }\t}} Web缓存web缓存是能够通过暂时存储Web对象,以减少服务延迟的技术。如果客户端频繁访问某几个页面,那么使用Web缓存存储这些Web对象会减少大量时延。 web缓存分类: 数据库缓存 CDN缓存 浏览器缓存 代理服务器缓存 代理服务器web缓存器(Web cache)也叫代理服务器(proxy server),它是能够代表初始Web服务器来满足HTTP请求的网络实体。 Web缓存器的运行如下: PC端浏览器会向代理服务器创建TCP连接,并向代理服务器发送一个HTTP请求。 如果代理服务器有PC浏览器请求的对象,代理服务器就能直接返回该对象。 如果没有请求的对象,代理服务器就会和初始服务器建立TCP连接,并发送一个HTTP请求。然后初始服务器会发送HTTP响应给Web缓存器。 当代理服务器中没有该对象时,它会在本地存储一个该对象副本,并向PC浏览器使用HTTP响应报文发送该副本(通过现有的PC浏览器和代理服务器的TCP连接)。 反向代理代理服务器除了作为Web缓存器外它还具有其他功能,比如拦截和过滤HTTP请求和响应、隐藏网络内部IP等。上面使用的代理服务器是作为正向代理。 正向代理是指代理服务器在客户端和服务器之间。客户端发送请求到代理服务器,代理服务器将请求转发给目标服务器,并将目标服务器的响应转发回客户端(代理服务器常与客户端在同一局域网下)。 正向代理常用于加强安全、缓存内容以加速访问、访问受限资源等场景。 代理服务器还能用于反向代理,反向代理是指代理服务器在服务器和客户端之间。客户端发送请求到反向代理服务器,反向代理服务器将请求转发给真实服务器,并将真实服务器的响应转发回客户端(反向代理服务器常与服务器在同一局域网下)。 反向代理常用于负载均衡、高可用性、加强安全等场景。 条件GET缓存能够很好减少用户感受到的响应时间,但是也引入了一个新问题:如果缓存中引入的副本已经过时了,或者说服务器上文件已经更新了但缓存的副本还没更新。 对此,HTTP使用了条件GET解决这种问题,它的解决方式如下: 允许代理服务器向服务器发送使用GET方法的请求报文 这个报文的请求头包含一个if-Modified-Since 当代理服务器从客户端接受到一个请求对象,这个对象正好存储在代理服务器中。此时代理服务器发现缓存一段时间没有更新,它会向服务器发送一个条件GET请求。如果服务器没有修改过该对象,响应报文是不会包含对象副本。 参考 计算机网络(自顶向下方法)第七版 终于有人把正向代理和反向代理解释的明明白白了!-腾讯云开发者社区-腾讯云 (tencent.com) WEB缓存 - 掘金 (juejin.cn)","tags":["golang","计算机网络","HTTP"],"categories":["计算机基础","网络"]},{"title":"合约安全(一)-数据存储","path":"/2024/07/15/合约安全(一)/","content":"存储布局了解以太坊存储布局有什么用在讨论这个问题前我们要知道以太坊存储布局是什么。以太坊储存布局是指合约状态变量或数据在EVM storage中存储的方式。 了解以太坊的存储布局我们能够: 对于未使用public,我们也能够获得其中的数据值 优化存储以减少gas开销 使用内联汇编直接操作storage 理解溢出,变量覆盖原理 以太坊存储-Storage在EVM深入中我们了解到以太坊通过key-value方式去存储数据的,读取数据也是从KV数据库中进行的。Storage中key和value大小都为32 byte,存储的方式与value字节大小相关。对于不同类型的值(值类型或引用类型),其存储方式也不同。 像映射和动态数组这种不知道其大小的类型,其存储过程要比值类型更加复杂 存储规则合约的状态变量以一种紧凑的方式存储在区块链存储中,以这样的方式,有时多个值会使用同一个slot。 除了动态大小的数组和映射mapping ,数据(值类型数据)的存储方式是从位置 0 开始连续放置在存储storage 中。 对于每个变量,根据其类型确定字节大小。 插槽(slot)是以太坊key-value的一个存储槽单位。 基本存储规则 存储插槽(storage slot) 的第一项会以低位对齐的方式储存。 值类型仅使用存储它们所需的字节。 如果存储插槽中的剩余空间不足以储存一个值类型,那么它会被存入下一个存储插槽。 结构体(struct)和数组数据总是会开启一个新插槽(但结构体或数组中的各元素,则按规则紧密打包)。 结构体和数组之后的数据也或开启一个新插槽。 对于使用继承的合约,状态变量的顺序由没有任何其他合约依赖的合约开始的 C3 线性顺序(C3-linearized order)决定。如果上述规则允许的话,不同合约的状态变量共享同一个存储槽。结构体和数组的元素依次存储,就好像它们是独立的值一样。 大端对齐(低位对齐)这里先复习一下计算机组成的知识,大端对齐(低位对齐)和小端对齐(高位对齐)。一般把数据左端称为高位,右端称为低位。slot插槽使用的是大端对齐(低位对齐) \t高内存地址放整数的高位,低内存地址放整数的低位,这种方式叫倒着放,术语叫小端对齐。 \t高内存地址放整数的低位,低内存地址放整数的高位,这种方式叫正着放,术语叫大端对齐。 紧凑存储solidity中部分值类型的存储是确定的,比如,uint1~uint256类型、bool类型。这些值都不会超过32 byte。在发现使用的存储没有超过32 byte时,以太坊会使用紧凑存储 紧凑存储:以太坊为了节省存储量,会将小于32字节变量类型的存储和后面的字段尽可能在一个slot中存储。 我们可以通过代码和remix调试器分析紧凑存储中数据在storage中的布局。 12345678910111213contract A{ uint32 v1; uint128 v2; uint256 v3; bool v4; function set(uint8 i) public { v1 = i; v2 = i + 1; v3 = i + 2; v4 = true; }} 通过上面代码,我们根据存储基本规则,v1和v2会被存储到同一slot,v3、v4独占一个存储slot。 调用set函数(输入i=1),使用调试器跳到函数最后一步,能看到storage中key-value分布和上图展示一致。 key=0,slot的左端8字节存储v1,7~23字节存储v2 key=1,slot全部用于存储v3 key=2,slot的左端1字节存储v4 紧凑存储原则能够有效降低存储占用,如果能有效利用该特性,就能够存储消耗的gas。比如将上面代码中v3和v4位置互换就能降低存储时消耗gas(可以在remix中尝试)。 动态数组和映射类型的存储规则当数据大小不可预知时,比如:动态数组和映射类型,它们的存储位置是通过Keccak256 哈希计算来确定。 假设:映射或动态数组根据上面基本存储规则最终可确定某个位置 p 。 对于动态数组,此插槽中会存储数组中元素的数量(字节数组和字符串除外)。 对于映射,该插槽未被使用(为空),但它仍是需要的,以确保两个彼此挨着 映射,他们的内容在不同的位置上 动态数组存储动态数组存在字符串和bytes这样特殊的动态数组,它们存储方式与动态数组有点差异,先分析字符串类型的存储方式。 字符串字符串和bytes是特殊的动态数组,它们在存储时会有特殊的方式。 如果字符串或bytes的长度小于31byte,其数据和长度会共用一个slot(p),且长度计算等于len * 2。 将数据长度单独存储在slot(p)中,存储的value等于len * 2 + 1。数组中数据存储在keccak256(p)中,下一个数据的存储key等于上一个数据key+1 solidity中使用utf-8对string进行编码,中文需要占用3个字节 1234contract A{ string public s1 = unicode"短字符串"; string public s2 = unicode"一个字符串类型,计算我的插槽位置在哪里";} 上面代码中,s1的长度和s1的数据共用一个slot。而s2的长度大于32字节,其长度为55 byte。 p为256位 keccak256(p=0x00…00)=0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563 keccak256(p=0x00…01)=0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 也可以使用线上的keccak256工具 使用remix进入调试器,跳到最后一步查看storage状态。和上图分析一致。 动态数组对于动态数组,数据类型是小于128位使用紧凑存储的方式存储在一个slot中。下面展示一个uint32的动态数组 数据长度存储在slot确定的位置p,计算数组第一个数据位置keccak256(p)。数组下一个数据是上一个数据的key+1 123contract B { uint32[] public v1 = [1,2,3,4,5,2,1,2,4,1];} 其大小为0x0a(10),数据(10 * 32 = 320 < 256 * 2)会占用两个slot。计算keccak256(p=0) = 0x290decd…63 如何确定 x[i][j] 元素的位置,其中 x 的类型是 uint24[][],计算方法如下(假设 x``本身存储在槽 ``p): 槽位于 keccak256(keccak256(p) + i) + floor(j / floor(256 / 24)) 且可以从槽数据 v``得到元素内容,使用 ``(v >> ((j % floor(256 / 24)) * 24)) & type(uint24).max. 映射存储字典的存储布局是直接存储 Key 对应的 value,每个 Key 对应一份存储。 映射mapping 中的键 k 所对应的槽会位于 keccak256(h(k) . p) ,其中 . 是连接符, h 是一个函数,根据键的类型: 值类型, h 与在内存中存储值的方式相同的方式将值填充为32字节。 对于字符串和字节数组, h(k) 只是未填充的数据。 123456contract C{ mapping (uint256 => uint256) public bal; function set(uint256 k, uint256 v) public { bal[k] = v; }} 首先使用set向mapping中存储数据。根据上述规则,映射存储规则与下图计算结果一致。 笔者使用的参数:0,8、1,1、2,2、3,3 然后使用调试器跳到最后一步查看storage中存储情况。 实践-计算数据位置实际编写合约场景中,合约数据类型可能会更加复杂,会存在多个值类型或引用类型的组合。但我们都可以通过以上规则计算每个数据对应的slot位置。 类似下图中合约的组合型的数据,我们可以逐步进行分析。 在进行实践时我们可以搭建本地node环境,自己编写一个合约进行验证,下面编写了几个简单的脚本去计算数据位置。 启动本地节点1234cd contractnpm init -ynpm install --save-dev hardhat #下载hardhatnpx hardhat node #启动节点 可以在hardhat中使用以下命令部署合约。需要编写部署脚本。但是调用函数需要另外一些脚本。 1npx hardhat run --network localhost scripts/deploy.js 更简单的是通过remix连接到本地网络部署合约。调用函数也更简单。 脚本下载ethers.js v6。 1234cd jsnpm init -ynpm install -s etherstouch getStorage.js 在创建的文件下编写脚本。 123456789101112131415const ethers = require("ethers")const provider = new ethers.JsonRpcProvider('http://localhost:8545');// 合约地址const addressBridge = 'address'// 合约所有者 slotconst slot = "slot_post"const main = async () => { const privateData = await provider.getStorage(addressBridge, slot) console.log("读出数据:", privateData) }main() 引用: 在windows下详解:大端对齐和小端对齐 - 黑泽君 - 博客园 (cnblogs.com) 状态变量在储存中的布局 — Solidity中文文档 — 登链社区 (learnblockchain.cn) CUIT靶场题库","tags":["以太坊","区块链","合约安全"],"categories":["区块链","以太坊EVM"]},{"title":"深入EVM(二)-EVM指令集","path":"/2024/07/12/深入EVM(二)/","content":"EVM指令集EVM中的代码是合约编译过来的字节码。其中构成字节码的一个EVM指令集大小为一个字节(8bit),所以EVM总指令集最多由256个,目前已存在100多个指令(5C-5E 没有使用)。 EVM执行从EVM Code中读取到的指令集,stack能从memory、calldata或storage一次加载一个字大小的数据(4byte),将指令使用的值放入栈内进行操作得到结果后将其值返回。 下面会从solidity汇编常用的指令去进行代码展示。EVM指令集详细细节查询:EVM Codes - An Ethereum Virtual Machine Opcodes Interactive Reference EVM指令提供了以下几个功能: 算术和位逻辑运算 执行上下文查询 堆栈、内存和存储访问 控制流操作 Logging, calling和其他操作符 算术运算123456789101112ADD //将堆栈顶部的两个项相加MUL //将堆栈顶部的两个项相乘SUB //减去堆栈顶部的两个项DIV //整数除法SDIV //有符号整数除法MOD //取模(余数)操作SMOD //有符号取模操作ADDMOD //对任意数字进行加法取模MULMOD //对任意数字进行乘法取模EXP //指数运算SIGNEXTEND //扩展二进制补码有符号整数的长度SHA3 //计算内存块的 Keccak-256 哈希 堆栈操作12345678910POP //从堆栈中移除顶部项MLOAD //从内存中加载一个字MSTORE //将一个字保存到内存中MSTORE8 //将一个字节保存到内存中SLOAD //从存储中加载一个字SSTORE //将一个字保存到存储中MSIZE //获取活动内存的大小(以字节为单位)PUSHx //将 x 字节项目放入堆栈,其中 x 可以是从 1 到 32(完整字)的任何整数DUPx //复制第 x 个堆栈项,其中 x 可以是从 1 到 16 的任何整数SWAPx //交换第 1 个和第(x +1)个堆栈项,其中 x 可以是从 1 到 16 的任何整数 流程控制操作码12345STOP //停止执行JUMP //将程序计数器设置为任何值JUMPI //有条件地更改程序计数器PC //获取程序计数器的值(在增量之前对应于此指令)JUMPDEST //标记跳转的有效目的地 系统操作12345678910LOGx //附加具有 x 个主题的日志记录,其中 x 是从 0 到 4 的任何整数CREATE //创建一个带有关联代码的新帐户CALL //消息调用到另一个帐户,即运行另一个帐户的代码CALLCODE //消息调用到此帐户与另一个帐户的代码RETURN //停止执行并返回输出数据DELEGATECALL //使用替代帐户的代码向此帐户发送消息调用,但保留当前发送者和价值的值STATICCALL //静态消息调用到一个帐户REVERT //停止执行,恢复状态更改但返回数据和剩余 gasINVALID //指定的无效指令SELFDESTRUCT //停止执行并注册帐户以进行删除逻辑操作:用于比较和位逻辑的操作码: 逻辑运算1234567891011LT //小于比较GT //大于比较SLT //有符号小于比较SGT //有符号大于比较EQ //相等比较ISZERO //简单的非运算AND //按位与操作OR //按位或操作XOR //按位异或操作NOT //按位非操作BYTE //从完整的 256 位宽字中检索单个字节 环境操作12345678910111213141516GAS //获取可用 gas 的数量(在减少此指令的 gas 后)ADDRESS //获取当前执行账户的地址BALANCE //获取任何给定账户的账户余额ORIGIN //获取启动此 EVM 执行的 EOA 的地址CALLER //获取立即负责此执行的调用者的地址CALLVALUE //获取由负责此执行的调用者存入的以太币金额CALLDATALOAD //获取由负责此执行的调用者发送的输入数据CALLDATASIZE //获取输入数据的大小CALLDATACOPY //将输入数据复制到内存CODESIZE //获取当前环境中运行的代码大小CODECOPY //将当前环境中运行的代码复制到内存GASPRICE //获取由发起交易指定的 gas 价格EXTCODESIZE //获取任何账户的代码大小EXTCODECOPY //将任何账户的代码复制到内存RETURNDATASIZE //获取当前环境中上一次调用的输出数据大小RETURNDATACOPY //将上一次调用的数据输出复制到内存 区块操作码123456BLOCKHASH //获取最近完成的 256 个区块之一的哈希COINBASE //获取区块奖励的区块受益人地址TIMESTAMP //获取区块的时间戳NUMBER //获取区块的编号DIFFICULTY //获取区块的难度GASLIMIT //获取区块的 gas 限制 编译合约工具准备笔者编译合约使用以下工具 solcjs: 使用solcjs能够获得合约的二进制代码。 123456# 安装solcjsnpm install solcjs# 查看版本solcjs -V # 指定合约二进制代码输出路径solcjs --bin --output-dir out example.sol Remix:Remix能够获得合约字节码和将字节码指定为操作码。 编译合约选用remix的代码案例进行分析。下面是一个存储合约,代码函数由两部分组成。 两个函数的选择器 store(uint256):6057361d retrieve():2e64cec1 1234567891011121314151617181920212223242526272829// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.8.2 <0.9.0;/** * @title Storage * @dev Store & retrieve value in a variable * @custom:dev-run-script ./scripts/deploy_with_ethers.ts */contract Storage { uint256 number; /** * @dev Store value in variable * @param num value to store */ function store(uint256 num) public { number = num; } /** * @dev Return value * @return value of 'number' */ function retrieve() public view returns (uint256){ return number; }} 字节码分析该部分字节码通过remix获得。object部分对应合约字节码,opcode部分对应字节码的操作码。 1234{\t"object": "608060405234801561000f575f80fd5b506101438061001d5f395ff3fe608060405234801561000f575f80fd5b5060043610610034575f3560e01c80632e64cec1146100385780636057361d14610056575b5f80fd5b610040610072565b60405161004d919061009b565b60405180910390f35b610070600480360381019061006b91906100e2565b61007a565b005b5f8054905090565b805f8190555050565b5f819050919050565b61009581610083565b82525050565b5f6020820190506100ae5f83018461008c565b92915050565b5f80fd5b6100c181610083565b81146100cb575f80fd5b50565b5f813590506100dc816100b8565b92915050565b5f602082840312156100f7576100f66100b4565b5b5f610104848285016100ce565b9150509291505056fea26469706673582212205b47cee55f559ebab97cf0c7dc7a6cef1aba6f07a98a0a25c74b22ba6701722364736f6c63430008160033",\t"opcodes": "PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0xF JUMPI PUSH0 DUP1 REVERT JUMPDEST POP PUSH2 0x143 DUP1 PUSH2 0x1D PUSH0 CODECOPY PUSH0 RETURN INVALID PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0xF JUMPI PUSH0 DUP1 REVERT JUMPDEST POP PUSH1 0x4 CALLDATASIZE LT PUSH2 0x34 JUMPI PUSH0 CALLDATALOAD PUSH1 0xE0 SHR DUP1 PUSH4 0x2E64CEC1 EQ PUSH2 0x38 JUMPI DUP1 PUSH4 0x6057361D EQ PUSH2 0x56 JUMPI JUMPDEST PUSH0 DUP1 REVERT JUMPDEST PUSH2 0x40 PUSH2 0x72 JUMP JUMPDEST PUSH1 0x40 MLOAD PUSH2 0x4D SWAP2 SWAP1 PUSH2 0x9B JUMP JUMPDEST PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 RETURN JUMPDEST PUSH2 0x70 PUSH1 0x4 DUP1 CALLDATASIZE SUB DUP2 ADD SWAP1 PUSH2 0x6B SWAP2 SWAP1 PUSH2 0xE2 JUMP JUMPDEST PUSH2 0x7A JUMP JUMPDEST STOP JUMPDEST PUSH0 DUP1 SLOAD SWAP1 POP SWAP1 JUMP JUMPDEST DUP1 PUSH0 DUP2 SWAP1 SSTORE POP POP JUMP JUMPDEST PUSH0 DUP2 SWAP1 POP SWAP2 SWAP1 POP JUMP JUMPDEST PUSH2 0x95 DUP2 PUSH2 0x83 JUMP JUMPDEST DUP3 MSTORE POP POP JUMP JUMPDEST PUSH0 PUSH1 0x20 DUP3 ADD SWAP1 POP PUSH2 0xAE PUSH0 DUP4 ADD DUP5 PUSH2 0x8C JUMP JUMPDEST SWAP3 SWAP2 POP POP JUMP JUMPDEST PUSH0 DUP1 REVERT JUMPDEST PUSH2 0xC1 DUP2 PUSH2 0x83 JUMP JUMPDEST DUP2 EQ PUSH2 0xCB JUMPI PUSH0 DUP1 REVERT JUMPDEST POP JUMP JUMPDEST PUSH0 DUP2 CALLDATALOAD SWAP1 POP PUSH2 0xDC DUP2 PUSH2 0xB8 JUMP JUMPDEST SWAP3 SWAP2 POP POP JUMP JUMPDEST PUSH0 PUSH1 0x20 DUP3 DUP5 SUB SLT ISZERO PUSH2 0xF7 JUMPI PUSH2 0xF6 PUSH2 0xB4 JUMP JUMPDEST JUMPDEST PUSH0 PUSH2 0x104 DUP5 DUP3 DUP6 ADD PUSH2 0xCE JUMP JUMPDEST SWAP2 POP POP SWAP3 SWAP2 POP POP JUMP INVALID LOG2 PUSH5 0x6970667358 0x22 SLT KECCAK256 JUMPDEST SELFBALANCE 0xCE 0xE5 PUSH0 SSTORE SWAP15 0xBA 0xB9 PUSH29 0xF0C7DC7A6CEF1ABA6F07A98A0A25C74B22BA6701722364736F6C634300 ADDMOD AND STOP CALLER ",} 合约部署使用remix的调试器对合约部署交易进行分析。合约部署时,EVM会执行到字节码第27字节时结束。下面开始分析合约部署时,opcode的逻辑。 合约部署相关操作码主要发生在第15-27字节之间。我们可以先通过前四个指令开始研究EVM指令集是怎样工作的。 首先push1是将1字节压入栈中,可以使用push32指令可以压入32字节到栈。指令进行了两次push1操作 1PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE 然后使用了mstore(offset,vaule)将0x08存入内存中。 下一个操作码是CALLVALUE,这是一个环境操作码,它将启动此执行的消息调用发送的以太数量(以wei为单位)推到堆栈的顶部。 合约部署时会通过codecopy将合约操作码copy到内存中,在代码执行到jump时,会跳转到0xf位置,即下面的JUMPDEST。 1JUMPDEST POP PUSH2 0x143 DUP1 PUSH2 0x1D PUSH0 CODECOPY PUSH0 RETURN 此时栈的状态如下: 让我们跳到最重要的一步:codecopy(destOffset, offset, size),该指令会将当前环境下运行的代码复制到内存中,其中destOffset是复制到内存的偏移量,offset是复制代码中的字节偏移量,size是复制的代码字节大小。 codecopy从栈顶拿到三个值,复制到代码0x00位置,从当前环境(calldata)中的1d位置开始,大小为143字节。可以看到memory中复制的字节码。 执行函数使用remix调用store函数,存入5到合约中。交易的calldata如下: 1"0x6057361d0000000000000000000000000000000000000000000000000000000000000005" 执行store函数时从第26字节开始,第26字节位置使用了calldataload(i)。calldataload是从calldata指定偏移i读取32字节到栈中。然后通过shr(shift, value)将value向右偏移0xE0位 1CALLDATALOAD PUSH1 0xE0 SHR calldata读到的是store的函数选择器,指令会比较函数选择器跳转到相应位置。下面两行是两个函数选择器,根据操作码逻辑,先使用dup对栈中0的值进行复制,然后push4 向栈中推入retrieve()的选择器进行比较,eq会返回0。 1DUP1 PUSH4 0x2E64CEC1 EQ PUSH2 0x38 JUMPI jumpI(counter, b)能跳转到其他指令,其中counter是将继续执行的部署代码中的字节偏移量(跳到的指令必须是JUMPDEST),b当该值不等于0是才会跳转。在下图中,栈顶的两个值为0x38,0x00,由于b等于0,jumpI不会跳转。 同上,但是jumpI操作码能够跳转到0x56位置,因为calldata传入的选择器与栈顶顶的值一致。(注意remix使用十进制对应指令位置,即跳转到86) 1DUP1 PUSH4 0x6057361D EQ PUSH2 0x56 JUMPI 现在我们直接跳到即将结束的一步,EVM会使用sstore(key,value)将给定的参数5存储在对应合约地址的key-0:value-5的位置。 使用sstore后storage变化 使用内联汇编减少gas消耗上面我们学习了指令集,不仅调试的时候可以使用它进行分析,我们还能够使用它在solidity中写内联汇编。可以通过assembly块开始一段代码。 123assembly{\t// EVM汇编语言} 下面进行一个简单的演示 1234567891011121314151617// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract assTest{ function add1(uint256 a, uint256 b) public pure returns (uint) { return a + b; } function add2(uint x, uint y) public pure returns (uint) { assembly { let result := add(x, y) mstore(0x0, result) return(0x0, 32) } }} 从remix的获得的两个函数消耗gas进行比较(可能因环境不同有差异) 前者使用:949 gas 后者使用:561 gas 引用: 1.ethereumbook/13evm.asciidoc at develop · ethereumbook/ethereumbook (github.com) 2.EVM Codes - An Ethereum Virtual Machine Opcodes Interactive Reference","tags":["以太坊","区块链","EVM"],"categories":["区块链","以太坊EVM"]},{"title":"深入EVM(一)-EVM架构","path":"/2024/07/11/深入EVM(一)/","content":"EVM架构EVM具有基于堆栈的体系结构,它将内存中的值都存储在堆栈上,然后使用PC读取堆栈的操作码去执行合约代码。 EVM可以分为三个可寻址组件: Virtual ROM:用于存放代码的虚拟只读存储器,在ROM中的code(智能合约)不能更改。 Machine state:相当于RAM,一种易失性的存储器。这里的易失的是指当合约代码执行完毕后,内存的变量会被清除。 World state:所有合约的状态变量存储的地方,这部分数据是永久存储在区块链上的数据。 Source: https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf EVM的字长为256位,这样方便本地哈希和椭圆曲线操作 Machine state堆栈(stack)EVM堆栈是用于存储字节码执行过程的中间数据和指令,所有的操作都是在堆栈上执行的。 EVM堆栈的大小是256bit * 1024=28K(字位宽为256,栈深度为1024),堆栈的读写都是以256bit为单位进行的。内联汇编可以使用PUSH、POP、SWAP、DUP指令操作stack。 Source: https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf 内存(memory)EVM内存是线性存储的,可以实现字节级别的寻址。它用于存储临时变量和一些动态大小的数组。solidity可以使用memory来声明内存变量。 内存的字宽是8位。内存一次能读取为256位,而内存一次写入可以为8位或256位。内联汇编能够使用 MSTORE、MSTORE8、MLOAD指令操作memory。 Source: https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf 程序计数器(PC)PC类似于汇编中的寄存器,它指向EVM将要执行的下一条指令。在执行一条指令后,PC通常会增加1 byte(32bit)。其中的例外情况包括JUMP操作码变体,它们将PC重新定位到堆栈顶部指定的位置。 World state存储(storage)在以太坊中,每个特定地址的智能合约都有自己的 “存储”,由一个键值存储组成,将 256 位Key映射到 256 位Value。可以通过solidity中定义storage和状态变量去指定变量存储位置。 存储的每次读写都是256bit。内联汇编可以使用SSTORE、SLOAD指令操作storage。 Source: https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf Virtual ROM合约代码(EVM code)合约代码是指EVM在本机执行的字节码。在EVM的ROM中只能对合约代码进行读操作。 字节码包含了很多关于合约的信息和逻辑,包括调度器,以及合约元数据。 Source: https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf Calldata根据以太坊黄皮书描述,calldata是一个不限制大小的字节数组,用来指定消息调用的输入数据(这个输入数据是指Massage Call输入的data)。calldata与内存不同,是一段只读的可寻址的保存函数调用参数的空间。 一个消息调用交易包括: \tdata: 一个不限制大小的字节数组,用来指定消息调 用的输入数据,由 Td 表示。 —节选以太坊黄皮书 当一个以太坊合约被外部或EOA调用时,调用者将数据传递给合约,这些数据被存储在calldata区域。在内存或者栈需要使用到该数据时,会通过calldata相关指令集取操作 对calldata操作的指令有三个: calldatasize:返回calldata的大小。 calldataload:从calldata中加载32bytes到stack中。 calldatacopy:拷贝一些字节到内存中。 Calldata组成calldata组成由两部分: 前四字节是函数选择器。 其余字节是函数输入参数, 每个输入参数长度为32字节。小于32字节的参数会被填充到32字节长度。 1234567891011// a = 1, b = 2// calldata: 0x04bc52f8(函数选择器)0000000000000000000000000000000000000000000000000000000000000001(a)0000000000000000000000000000000000000000000000000000000000000002(b) function example( uint256 a, uint256 b) public pure returns (bytes memory result) { result = abi.encodeWithSignature("foo(uint256,uint256)", a, b); } Calldata特性calldata 区域是只读的,合约不能在执行期间修改它。 读取calldata其实是从calldata区域复制其中的值到堆栈中。 比如:声明一个calldata数组,如果试图修改calldata的参数就会报错。 Messgae Call在调用者(EOA或合约)发送一个Messgae Call时,会有一个calldata被放入交易的data字段。EVM会为data分配一个只读的空间(大小没有限制) 。EVM能够通过读取calldata中的函数选择器知晓合约中那一个函数的代码能执行。 该图是一个合约之间的Message Call 代码示例代码逻辑 msg.data:可以获得整个calldata数据 event Log(bytes data):返回当前交易的calldata 逻辑:用户地址调用合约参数A.example1,输入合约B地址和a,b的值,在example1中会出发Log事件。然后example1会使用call调用B中的add函数,并触发Log事件 预测:根据calldata的原理,第一个触发事件会返回example1的函数选择器和a,b两个参数;第二个触发的事件会返回add的函数选择器和a,b两个参数;其中result和第二个Log中的data值相同 12345678910111213141516171819202122232425// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract A { event Log(bytes data); function example1(address crt,uint256 a, uint256 b) public returns (bytes memory result, bytes memory data) { emit Log(msg.data); result = abi.encodeWithSignature("add(uint256,uint256)", a, b); bool success; (success, data) = crt.call(result); }}contract B { event Log(bytes data); function add(uint256 a, uint256 b) public returns (uint256) { emit Log(msg.data); return a + b; }} 事件和结果输入参数:crt=b_addres,a=1,b=5 第一个Log的data前四位字节为:0xebe5f989 第二个Log的data前四位字节为:0x771602f7 返回的result前四位字节为:0x771602f7,且result的值也与data完成相同 引用 1.ethereumbook/13evm.asciidoc at develop · ethereumbook/ethereumbook (github.com) 2.EVM深度分析之数据存储(一) 3.深入了解Solidity数据位置 - Calldata 4.ethereum_yellow_paper_cn.pdf","tags":["以太坊","区块链","EVM"],"categories":["区块链","以太坊EVM"]},{"title":"币安钱包运作原理","path":"/2024/05/28/币安钱包运作原理/","content":"加密钱包加密货币钱包是一种与区块链网络进行交互的工具,可以用于区块链各网络的转账交易、合约交互和密钥管理等功能。 常见加密货币钱包分为三类: 软件钱包:用户通过应用软件管理密钥和于区块链的交互,其类型又可以分为:桌面端钱包、网络钱包、移动端钱包。可以简单的理解这三种类型的钱包。 桌面端钱包是下载到本地计算机的软件 网络钱包多是浏览器的扩展件 移动钱包是为智能手机量身定做的软件 硬件钱包:硬件钱包是一种实体电子设备,使用随机数生成器(RNG)生成公钥以及私钥。 纸钱包:通过物理方式将将密钥和地址记录在纸上或其他物理实体上,但这种方式无疑是过时的(某种意义上讲,只要保管的好就能保存很久——纸只要不受潮能比硬件设备寿命得还久…) 根据其工作机制,还可将其分为热钱包或冷钱包。一般通过是否与网络连接来区分钱包是冷钱包还是热钱包。 冷钱包和热钱包冷钱包是一种不会连接网络,也不会于任何智能合约交互的加密钱包。一般冷钱包是存放在离线的电子设备上,比如:Ledger 售卖的设备、安装移动端钱包的不联网手机都是冷钱包(冷钱包不等于硬件钱包,硬件钱包可以是冷钱包) 冷钱包不会连接网络其私钥对于网络来说是保持离线状态的,所以冷钱包很难遭受到黑客的攻击 由于冷钱包的安全性,它适合用于大额代币的资产存储 冷钱包只能转账不会与智能合约交互,这可以保证钱包避免恶意合约带来的损失 冷钱包通过与你自己的热钱包签署交易进行转账 热钱包是与区块链网络连接进行交易或与合约交互的加密钱包。与冷钱包相比,热钱包具有更多的灵活性,它可以随时向网络发送交易和与合约交互,所以它需要保持与网络连接。网络钱包和移动端钱包多是热钱包,比如:metamask、okt、coinbase等 热钱包需要连接网络,黑客可以通过网络侵入你的设备盗取私钥 热钱包适合日常使用和小额交易 冷钱包由于其离线的特性,它不能查看到自己钱包中的余额。但是可以通过热钱包查询到冷钱包的余额 可以使用热钱包与冷钱包的组合,既能保证交易的灵活性也能保证钱包的安全性 币安钱包管理体系 大型加密资产交易平台的钱包管理体系异常复杂,以至于大多数人都无法理解。币安通过冷热钱包动态运行来确保必要的流动性,以实时满足每个订单的需求,同时减少潜在的安全性威胁。币安钱包管理系统的两大特点(流动性和安全性)是币安钱包运行的两大基础。 币安冷热钱包运作体系用户可以可以在各个区块链上向充值钱包充值加密货币,币安则通过监控区块链来获取用户的余额情况。 Consolidation——归集在完成链上认证后,币安会每隔一段时间将这些代币转移到其拥有的热钱包或冷钱包(在网络gas较低时进行转移)。这种将代币转移到综合*(omnibus)*热钱包的过程称为归集 币安会积累大量金额后进行转移 归集不会影响用户存取 通过*”归集”*流程整合热钱包中的资金可以确保快速、低成本地满足提现请求。 溢出和补充币安通过overflow(溢出)和top-up(补充)进行冷热钱包之间资金流转。 溢出是指多余资金将从热钱包流转到冷钱包或线下钱包等安全的储存介质中。 归集获得的大额资金时,会将部分资金从热钱包转移到冷钱包中。这种方式能增强资金的安全性 补充是指热钱包需要补充资金,将冷钱包的资金装转移到热钱包中。 用户向热钱包发起大额资金提现时,热钱包会从冷钱包补充资金。这种方式可以保证资金的流动性 PoR储备金证明保证币安资金透明性PoR——储备量证明,币安通过展示证据证明币安拥有覆盖所有用户资产 1:1 的资金以及一些储备金。 币安通过merkle tree和zk-SNARK(Zero-Knowledge Succinct Non-Interactive Argument of Knowledge,零知识证明)实现PoR 通过使用 zk-SNARK技术方案,加密货币交易所可以证明所有默克尔树树叶节点的余额集(即用户账户余额)构成了交易所公布的用户总资产余额。每个用户都可以轻松访问它的叶节点,因为该节点已包含在流程中。对于每位用户的余额( 默克尔树的叶节点),币安将确保: 1、每个用户的资产余额都被包含在币安的用户净余额总和里。 2、用户的总净余额大于或等于零。 3、将用户信息更新到叶节点哈希后,默克尔树根的更改是有效的(即,无法伪造信息)。 引用 加密资产在币安钱包间流转的方法和原因 What Is a Crypto Wallet and How to Choose the Right One? 储备金证明","tags":["区块链","交易所","binance"],"categories":["区块链","钱包开发"]},{"title":"HD钱包原理-BIP32、BIP39、BIP44","path":"/2024/05/27/HD钱包/","content":"HD钱包HD Wallet(Hierarchical Deterministic Wallet,分层确定性钱包)是基于[BIP32](bips/bip-0032.mediawiki at master · bitcoin/bips (github.com))实现的用于管理私钥的工具。 非确定性钱包和确定性钱包非确定性钱包可以参考比特币客户端使用随机生成密钥的方式。 比特币客户端会预先生成100个随机私钥缓存在一个密钥池,从最开始就生成足够多的私钥并且每个密钥只使用一次。这种钱包难以管理、 备份以及导入密钥。非确定性钱包的每一个密钥都需要进行备份,如果钱包不可访问时,没有备份的密钥就会失去其资金的控制权 确定性钱包中通过一个主密钥派生出子密钥,这个主密钥称为种子(seed)。 HD钱包是确定性钱包的一种衍生,HD钱包遵循BIP32标准,它通过seed导出树状结构的密钥,使得父密钥可以衍生一系列子密钥,每个子密钥又可以衍生出一系列孙密钥,以此类推,无限衍生。 与HD钱包相关的BIPBIP32、BIP39和BIP44是Bitcoin Improvement Proposals(比特币改进提案)中定义的三种标准,用于增强比特币和其他加密货币钱包的功能和安全性。 BIP32描述了比特币分层确定性钱包 BIP39描述了助记词的实现 BIP43和BIP44,这两个协议规定了钱包的树结构,即HD 钱包标准路径(主要分析BIP44) BIP32-HD原理BIP32由以下几部分来进行深入: 密钥序列化格式:密钥的数据结构 密钥派生:规定子密钥是如何从父密钥中派生 密钥树:父密钥派生子密钥所形成的树状结构 下面通过理论与代码(golang)相结合的方式去理解BIP32。 BIP32实现代码:go-bip32/bip32.go at master · tyler-smith/go-bip32 (github.com) 序列化格式:密钥结构BIP32规定密钥的序列化格式包括6个类型: 版本(version):4 byte,对密钥版本的预定。主网的公钥版本-0x0488B21E,私钥版本-0x0488ADE4(测试网 公钥版本-0x043587CF,私钥版本-0x04358394) 深度(depth):1 byte,密钥当前的层级。如:主密钥层级为0(0x00),派生的一级子密钥层级为1 (0x01) 父密钥指纹(FingerPrint):4 byte,父密钥hash值的前四位。主密钥的父指纹为0x00000000 子编号(child number):4 byte,密钥的索引 链码(chain code):32 byte,通过HMAC-SHA512计算的得到右32字节 公钥或私钥 (Key): 33 byte ,通过HMAC-SHA512计算的得到左32字节 123456789type Key struct {\tKey []byte // 33 bytes\tVersion []byte // 4 bytes\tChildNumber []byte // 4 bytes\tFingerPrint []byte // 4 bytes\tChainCode []byte // 32 bytes\tDepth byte // 1 bytes\tIsPrivate bool // unserialized} 密钥派生:生成子密钥过程密钥派生有很多种方式推导密钥,比如:父私钥→子私钥、父公钥→子公钥、父私钥→子公钥以及一种不可行的方式,父公钥→子私钥。下面从父私钥推导子公钥的过程进行描述 扩展密钥用于计算子密钥的一部分 将256位的私钥或公钥扩展为512位的位串,将左256表示为k, 右256位表示为c,得到扩展密钥 (k,c)。其中,k是密钥序列化中的key,c是链码 强化密钥得到密钥的种类,有根据给的索引大小区分普通密钥和强化密钥 为什么需要强化密钥(强化派生):当黑客拿到你的未硬化的扩展子私钥和扩展父公钥(链码)可以反向推导出父私钥或者所有的姊妹钱包,从而盗取你账户的所有资产 细节深入:HD Wallets: Why Hardened Derivation Matters? | by Blaine Malone | Medium 每个扩展密钥有 2 31个普通子密钥, 2 31个强化子密钥(hardened child keys)。 这些子密钥都有一个索引。 普通子密钥使用索引0到 2 31-1。 强化子密钥使用索引 2 31 到 2 31-1。 为了简化强化子密钥索引的符号,数字i H表示i + 2 31。 父私钥 → 子私钥 子密钥派生函数(Child key derivation):CKDprev( (k,c) , i ),其中i是钱包索引,这个索引与构建密钥树有关 输入扩展密钥与索引 i 将CKD中参数传入到HMAC-SHA512计算 判断(k,c)是否是强化密钥,比较i的值:i > 2 31 如果i > = 2 31, data = 0x00 || k || i (注意:0x00将私钥补齐到33字节长) i < 2 31,data = k || i 得到 l = CKDprev( (k,c) , i ),l 是一个长度为 512 的位串。将l 分为两部分lL , lR 得到子密钥:Ki = (lL + k) mod n 条件: lL < n ,Ki != 0 , len(Ki ) != 32 (否则得到密钥是无效的) 这里的n是指secp256k1标准定义的参数(Integers modulo the order of the curve, 简称:n) 得到子密钥链码:Ci = lR 代码分析创建主密钥主密钥通过种子得到,使用hmac函数计算出位串(Key =“比特币种子”,Data = seed),取lL 作为子密钥,lR作为链码 12345678910111213141516171819202122232425func NewMasterKey(seed []byte) (*Key, error) {\t// Generate key and chaincode\thmac := hmac.New(sha512.New, []byte("Bitcoin seed"))\t_, err := hmac.Write(seed)\tintermediary := hmac.Sum(nil)\t// Split it into our key and chain code\tkeyBytes := intermediary[:32]\tchainCode := intermediary[32:]\t// Create the key struct\tkey := &Key{ Version: PrivateWalletVersion, ChainCode: chainCode, Key: keyBytes, Depth: 0x0, ChildNumber: []byte{0x00, 0x00, 0x00, 0x00}, FingerPrint: []byte{0x00, 0x00, 0x00, 0x00}, IsPrivate: true,\t}\treturn key, nil} 父私钥推导子密钥用seed生成主密钥后,调用该方法去生成一个子密钥 123456789101112131415161718192021222324252627282930313233343536373839404142// NewChildKey derives a child key from a given parent as outlined by bip32func (key *Key) NewChildKey(childIdx uint32) (*Key, error) {\t// Fail early if trying to create hardned child from public key\tif !key.IsPrivate && childIdx >= FirstHardenedChild { return nil, ErrHardnedChildPublicKey\t}\tintermediary, err := key.getIntermediary(childIdx)\tif err != nil { return nil, err\t}\t// Create child Key with data common to all both scenarios\tchildKey := &Key{ ChildNumber: uint32Bytes(childIdx), ChainCode: intermediary[32:], Depth: key.Depth + 1, IsPrivate: key.IsPrivate,\t}\t// Bip32 CKDpriv\tif key.IsPrivate { childKey.Version = PrivateWalletVersion fingerprint, err := hash160(publicKeyForPrivateKey(key.Key)) if err != nil { return nil, err } childKey.FingerPrint = fingerprint[:4] childKey.Key = addPrivateKeys(intermediary[:32], key.Key) // Validate key err = validatePrivateKey(childKey.Key) if err != nil { return nil, err } // Bip32 CKDpub\t} else { // ...\t}\treturn childKey, nil} 通过扩展密钥计算lL , lR的位串使用HMAC-SHA512算法计算,先要对索引进行比较,密钥是否是强化密钥 123456789101112131415161718192021222324252627func (key *Key) getIntermediary(childIdx uint32) ([]byte, error) {\t// Get intermediary to create key and chaincode from\t// Hardened children are based on the private key\t// NonHardened children are based on the public key\tchildIndexBytes := uint32Bytes(childIdx)\tvar data []byte // FirstHardenedChild = uint32(0x80000000)\tif childIdx >= FirstHardenedChild { data = append([]byte{0x0}, key.Key...)\t} else { if key.IsPrivate { data = publicKeyForPrivateKey(key.Key) } else { data = key.Key }\t}\tdata = append(data, childIndexBytes...)\thmac := hmac.New(sha512.New, key.ChainCode)\t_, err := hmac.Write(data)\tif err != nil { return nil, err\t}\treturn hmac.Sum(nil), nil} 计算Ki使用该公式计算 Ki = (lL + k) mod n 1234567891011121314151617func addPrivateKeys(key1 []byte, key2 []byte) []byte {\tvar key1Int big.Int\tvar key2Int big.Int\tkey1Int.SetBytes(key1)\tkey2Int.SetBytes(key2)\tkey1Int.Add(&key1Int, &key2Int)\tkey1Int.Mod(&key1Int, curve.Params().N)\tb := key1Int.Bytes()\tif len(b) < 32 { extra := make([]byte, 32-len(b)) b = append(extra, b...)\t}\treturn b} 钱包结构:HD钱包的路径前面我们通过CKD函数计算出密钥,计算密钥需要两个参数一个是扩展密钥,一个是索引。通过这个索引我们能够沿着一条路径构建出子密钥,比如:构造一个路径为 “m/0/0/1” 的密钥来控制钱包 构造过程,从seed得到主密钥m,通过m去构造路径: CKD( CKD( CKD( m, 0), 0 ), 1) = CKD(CKD(m,0), 0)/1 = CKD(m, 0)/0/1 = m/0/0/1 Source: https://github.com/bitcoin/bips/blob/master/bip-0032/derivation.png BIP39-助记词标准BIP39描述了记助词的实现,主要分为两个部分:生成记助词和将记助词转化为二进制种子 为什么需要记助词:BIP32通过输入一个seed就能输出主密钥和一堆子密钥,我们只需记住seed就能控制钱包,但是对于人类来说记忆一串毫无关联的数字是很困难的。所以BIP39提出通过一组容易记住的单词(或者说一个句子)来用于生成seed。 生成记助词记助词生成过程: 生成一个初始熵(entropy),熵的长度在128~256位且必须为32位的倍数。其中ENT(entropy length)= 熵的长度 将初始熵进行SHA256计算,取前 ENT/32 位作为校验和(checksum)。其中 CS(checksum length)= ENT/32 将熵和校验和拼接(校验和附加在熵后),这个串会被分为11位一组,每一组会对应单词表的索引(0-2047),一共有 MS (mnemonic sentence) = ( ENT + CS ) / 11 组单词 单词表实现:python-mnemonic/src/mnemonic/wordlist/english.txt at master · trezor/python-mnemonic (github.com) ENT CS ENT + CS MS 128 4 132 12 160 5 165 15 192 6 198 18 224 7 231 21 256 8 264 24 Source: https://github.com/ethereumbook/ethereumbook 记助词生成种子记助词生成seed时需要两个参数:记助词(mnemonic)和盐(salt,也能叫密码,passphrase)。salt能够保护钱包,比如:黑客必须同时获得你的记助词和密码才能拿到生成主密钥的seed 生成seed的过程 向PBKDF2函数输入参数:记助词、盐 PBKDF2函数计算得到一个512位的seed Source: https://github.com/ethereumbook/ethereumbook BIP44-HD钱包路径标准BIP32中描述了HD钱包的结构,其每一层大约有40亿的子密钥和40亿的强化密钥而每一层又能继续衍生下去,这导致钱包里账户的路径近乎是无限的。如果没有一个明确的标准去约束密钥派生的路径,那么更换钱包时就可能出现兼容性问题 强化派生路径表示例子: m/1’/0’ , 其路径上有 “‘“作为强化派生的标记 路径级别BIP44规定的路径有五个级别 m/purpose’/coin_type’/accout’/change/address_index purpose:协议BIP44,一般设置为常量44’ (0x8000002C) coin_type:币种类型,比特币为0’ (0x80000000) accout:账户类型,为用户划分不同身份。从0’开始递增 change:钱包地址对外部是否可见,0用于外部链,1用于内部链 index:地址索引,地址从索引0开始按顺序递增编号 引用: https://github.com/the-web3/blockchain-wallet/tree/master/basicWallet [Web3专题(三) 2种钱包之分层确定性钱包(HD Wallet,BIP32,BIP39,BIP44) | 登链社区 | 区块链技术社区 (learnblockchain.cn)](https://learnblockchain.cn/article/7098#实现一个以太坊钱包(符合 BIP-44 标准的路径)) BIP: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki https://github.com/ethereumbook/ethereumbook 吐槽:为什么国内都不喜欢把引用的图源和链接注释出来…","tags":["以太坊","钱包","golang"],"categories":["区块链","钱包开发"]},{"title":"Uniswap-v1原理和源码分析","path":"/2024/05/20/Uniswapv1介绍和源码分析/","content":"Uniswap介绍介绍引用官方文档的定义 The Uniswap protocol is a peer-to-peer system designed for exchanging cryptocurrencies (ERC-20 Tokens) on the Ethereum blockchain. The protocol is implemented as a set of persistent, non-upgradable smart contracts; designed to prioritize censorship resistance, security, self-custody, and to function without any trusted intermediaries who may selectively restrict access. Uniswap是一个点对点的去中心化加密货币(或者说是符合ERC20协议的货币)交易系统,用户能使用这个交易所自由地完成各种ERC20 token之间或ETH的兑换。 应用链接:https://app.uniswap.org/ 发展历程Uniswap想法诞生:2017年6月22日,Vitalik发表文章《On Path Independence》 Uniswap创世人登场:2017年7月6日,Hayden Adams被西门子解雇,在其朋友Karl Floersch的影响下开始关注以太坊,并学习智能合约开发。 Uniswap-v1面世:2018年,在Hayden Adams与以太坊有关的人士讨论不断完善并在自身努力下,Uniswap-v1版本于11月2日部署到以太坊主网。 Uniswap的发展:自2020年后,Uniswap逐渐推出新的版本从v2到如今处于开发中的v4版本。Uniswap已经成为以太坊上交易量最大的DEX之一。 人物介绍:Hayden Adams ,是 Uniswap 的创始人之一,也是该项目的重要推动者和领导者。Adams 在加利福尼亚州长大,拥有工程和计算机科学的背景。 Adams 在2018年创建了 Uniswap,并在其初期的开发过程中积极参与。他在设计 Uniswap 时采用了自动化市场制造商(AMM)的概念,使得任何人都可以提供流动性,并从交易费中获取收益,而无需依赖传统的市场制造商或中介。 随着 Uniswap 的发展壮大,Adams 的领导和技术指导成为了项目成功的重要因素之一。他致力于推动项目的不断创新和发展,并与团队合作,不断改进 Uniswap 协议,以满足用户和市场的需求。 Adams 的努力和才华使得 Uniswap 成为了全球范围内 DeFi 生态系统中最重要和受欢迎的项目之一,同时也为加密货币和区块链技术的发展做出了重要贡献。 (人物介绍由ChatGPT生成,详细的人物故事和Uniswap的诞生过程可参考《A Short History of Uniswap》) Uniswap-v1原理自动做市商(automated market makers,AMM)在上述Uniswap的发展历程中中介绍到其设计灵感来源于《On Path Independence》,该文章描述了链上做市商的方式——即,AMM。 AMM是组成DeFi生态系统的一部分,它以一个简单的数学公式(恒定乘积公式,Constant Product Formula)为核心实现无许可和自动化的数字资产交易。 流动性(Liquidity):在AMM中的流动性是指将一种资产转化为另一种资产,这里资产可以是以太坊上的erc20代币 流动性提供者(Liquidity Provider,LP):LP也可认为是做市商,LP一般会为添加一对代币池子以供交易。LP可以通过向交易者收交易手续费,作为无偿损失的补偿。 恒定乘积公式恒定乘积公式:x*y = k x和y代表流动性池中的token的总量 不管交易怎样变化,市场始终无法脱离这条曲线 交易失去token和获得token只与交易前后的状态有关,与交易价格变化路径无关 通过恒定乘积公式我们能计算交易者swap买卖一种token获得另一种token的数量、LP为流动性池子添加流动性和移除流动性获得的Share 这里的share是指实现AMM的合约发行的token,这个token也可以理解为LP为池子的流动性量化体现。计算:share = √ ( x * y ) = √ k。 LP可以通过添加流动性mint share,通过移除流动性burn share LP取回池中的一定数量的token对(使用移除流动性方法),需要通过恒定乘积公式去burn一定数量的share 添加流动性LP向池中添加流动性,根据添加token对x与y的数量去计算需要mint的share数量。我们需要保证添加流动性时,池中价格保持不变,即p(添加前) = x/y 等于p(移除后) (x+dx)/(y+dy) 交易不管交易者在池中买卖token多少token,始终满足x * y = k。我们可以通过该公式计算卖dx计算得到dy 移除流动性当LP想要拿回池中的token对时,需要移除池中的流动性。我们需要确保移除流动时保持价格不变,即p(移除前) = x/y 等于p(移除后) (x-dx)/(y-dy) Uniswap-v1代码Uniswap v1使用vyper语言编写,这里我将使用solidity重构Uniswap的代码对其进行分析 v1的代码有两部分:Exchange和Factory Exchange:Token和ETH代币对,相当与一个池子,能进行添加、移除流动性和uniswap Factory:用与创建新的Toekn与ETH的代币对,也可以查询代币对对应的Exchange Exchangeexchange实现了ERC20标准,该合约通过添加ETH和Token流动性铸造代币,移除流动性销毁代币以获得ETH和Token。(合约铸造铸造代币下文成为LPT)此外合约提供了ETH与Token的代币兑换、Token与ETH的代币兑换、Token与ETH的价格和Token与另一种Token的兑换等方法 添加/移除流动性addLiquidity(uint256 min_liquidity, uint256 max_tokens, uint256 deadline) 向资金池添加流动性 min_liquidity:用户期望获得最小的LP代币,如果铸造代币过少交易会回滚 max_tokens:用户提供的最大Token数量,如果计算出花费Token大于这个值交易会回滚 deadline:交易确认时间,交易超过该时限会回滚 代码逻辑: 检查时限是否超过 用户添加ETH和Token数量不能为零 添加流动性有两种情况: 添加流动性:通过添加流动性公式计算出花费Token的数量和能铸造出的LPT,如果花费Token数量大于max_tokens或能铸造出的LPT小于min_liquidity就需要回滚交易 添加初始流动性:池子最初价格有该笔交易决定,铸造的LPT与转移的ETH数量一致,添加的Token数量等于max_tokens 需要将token转移到合约地址(transferFrom) 12345678910111213141516171819202122232425262728293031323334353637383940function addLiquidity( uint256 min_liquidity, uint256 max_tokens, uint256 deadline ) payable external returns (uint256) { require(deadline > block.timestamp, "block time exceeded deadline"); require(msg.value > 0 && max_tokens > 0, "add token or eth of amount can't equal zero"); uint256 total_liquidity = totalSupply(); if (total_liquidity > 0) { require(min_liquidity > 0, "min_liquidity can't equal zero"); // eth_reserve = x, token_reserve = y uint256 eth_reserve = address(this).balance - msg.value; uint256 token_reserve = token.balanceOf(address(this)); // token_amount = dy = dx/x * y + 1 uint256 token_amount = msg.value * token_reserve / eth_reserve + 1; // eth_amount = S = dx/x * T uint256 liuiqity_minted = msg.value * total_liquidity / eth_reserve; require(max_tokens > token_amount, "max_token does not meet the addLiquidity requirement"); require(liuiqity_minted > min_liquidity, "min_liquidity does not meet the addLiquidity requirement"); // mint liquidity _mint(msg.sender, liuiqity_minted); require(token.transferFrom(msg.sender, address(this), token_amount), "transferFrom failed"); emit AddLiquidity(msg.sender, msg.value, token_amount); return liuiqity_minted; } else { require(msg.value >= 1000000000, "init_liquidity must input 1 ether"); require(factory.getExchange(address(token)) == address(this), "factory not create the pair"); uint256 token_amount = max_tokens; // initial liquidity = x wei uint256 initial_liquidity = address(this).balance; // mint liquidity _mint(msg.sender, initial_liquidity); require(token.transferFrom(msg.sender, address(this), token_amount), "transferFrom failed"); emit AddLiquidity(msg.sender, msg.value, token_amount); return initial_liquidity; } } removeLiquidity(uint256 amount, uint256 min_eth, uint256 min_tokens, uint256 deadline)向资金池移除流动性 amount:想要销毁LPT数量 min_eth:用户最少能取出的ETH数量,如果能取出的最小ETH小于该值,交易会回滚 min_tokens:用户最少能取出的Token数量,如果能取出的最小Token小于该值,交易会回滚 deadline:交易确认时间,交易超过该时限会回滚 代码逻辑与添加流动性类似 检查输入参数是否满足条件,amount,min_eth,min_tokens大于零,当前时间没有时限。 计算给定销毁shares能够取出多少Token和ETH 检查取出的Token和Eth是否满足最小期望 1234567891011121314151617181920212223242526function removeLiquidity( uint256 amount, uint256 min_eth, uint256 min_tokens, uint256 deadline ) payable external returns (uint256 eth_amount, uint256 token_amount) { require(amount > 0, "removeLiquidity must greater than zero"); require(deadline > block.timestamp, "block time exceeded deadline"); require(min_eth > 0 && min_tokens > 0, "add token or eth of amount can't equal zero"); uint256 total_liquidity = this.totalSupply(); require(total_liquidity != 0, "total_liquidity equal zero"); uint256 token_reserve = token.balanceOf(address(this)); // eth_amount = dx = x * S / T eth_amount = address(this).balance * amount / total_liquidity; // token_amount = dy = y * S / T token_amount = token_reserve * amount / total_liquidity; require(eth_amount > min_eth && token_amount > min_tokens, "min_token or min_tokens does not meet the removeLiquidity requirement" ); // burn liquidity _burn(msg.sender, amount); payable(msg.sender).transfer(amount); require(token.transfer(msg.sender, token_amount), "transferFrom failed"); emit RemoveLiquidity(msg.sender, eth_amount, token_amount); } 价格查询基础 getInputPrice(uint256 input_amount, uint256 input_reserve, uint256 output_reserve) -> uint256 123456789101112131415161718function getInputPrice( uint256 input_amount, uint256 input_reserve, uint256 output_reserve ) private view returns (uint256) { require(input_reserve > 0 && output_reserve > 0, "input or output equal zero"); // fee < 1000, not write require logic uint256 input_amount_with_fee = 1 wei; input_amount_with_fee = input_amount * (1000 - fee); uint256 numrator = input_amount_with_fee * output_reserve; uint256 denominator = ( input_reserve * 1000 ) + input_amount_with_fee; // dy = y * dx * (1000-fee) / (x * 1000 + dx * (1000 -fee)) return numrator / denominator; } getOutputPrice(output_amount: uint256, input_reserve: uint256, output_reserve: uint256) -> uint256 12345678910111213function getOutputPrice( uint256 output_amount, uint256 input_reserve, uint256 output_reserve ) private pure returns (uint256) { require(input_reserve > 0 && output_reserve > 0, "input or output equal zero"); uint256 numrator = input_reserve * output_amount * 1000; uint256 denominator = (output_reserve - output_amount) * 997; return numrator / denominator + 1; } 以上面两个函数为基础得到计算eth与toekn之间的价格 getEthToTokenInputPrice(eth_sold: uint256(wei)) -> uint256: 输入要卖出的ETH数量,返回能得到的token数量 getEthToTokenOutputPrice(tokens_bought: uint256) -> uint256(wei):输入要买的token数量,返回需要给出的ETH数量 getTokenToEthInputPrice(tokens_sold: uint256) -> uint256(wei):输入要卖的token数量,返回得到的token数量 getTokenToEthOutputPrice(eth_bought: uint256(wei)) -> uint256输入得到的ETH数量,返回需要的token数量 兑换exchange的其他函数包括有:ETH与Token的兑换、Token与ETH的兑换、Token与Token之间的兑换 代码通过内部ethToTokenInput、ethToTokenOutput、tokenToEthOutput等函数兑换逻辑,一般先获得可以买/卖的代币/eth数量,确认能获得预期数量的token/eth,然后进行转账交易. 这里粗滤介绍下v1代码功能,因为v1使用的人太少加之v1是最早的版本,具体的操作和测试留到v2进行。关于代码具体细节可以看源码,或者重构的代码。 ETH与Token的兑换 ethToTokenSwapInput ethToTokenTransferInput ethToTokenOutput ethToTokenSwapOutput Token与ETH的兑换 tokenToEthSwapInput tokenToEthTransferInput tokenToEthSwapOutput tokenToEthTransferOutput Token与Token之间的兑换 tokenToTokenSwapInput tokenToTokenTransferInput tokenToTokenSwapOutput tokenToTokenTransferOutput tokenToExchangeSwapInput tokenToExchangeTransferInput tokenToExchangeSwapOutput tokenToExchangeTransferOutput Factoryfactory通过createExchange创建exchange,一个代币创建一个exchange 1234567891011function createExchange(address token) public returns(address) { require(token != address(0)); require(token_to_exchange[token] == address(0)); Exchange exchange = new Exchange(token, 3); token_to_exchange[token] = address(exchange); exchange_to_token[address(exchange)] = token; uint256 token_id = tokenCount + 1; id_to_token[token_id] = token; emit NewExchange(token, address(exchange)); return address(exchange); } 参考资料梁培利的个人空间-梁培利个人主页-哔哩哔哩视频 (bilibili.com) Uniswap V1 原理与源码解析: https://juejin.cn/post/7172903744147980325 Uniswap源码仓库:https://github.com/Uniswap/v1-contracts 重构源码仓库地址:https://github.com/Salbt/Uniswap-v1-solidity","tags":["uniswap","defi","源码分析","code"],"categories":["ethereum","DeFi","Uniswap"]},{"title":"Maker协议介绍","path":"/2024/05/12/Maker-Introduce/","content":"DAI稳定币:由于比特币、以太坊这些加密货币的价值波动太大,不能在正常生活中作为现金使用。所以需要一些具有稳定价值的加密货币进行日常的交易和使用。 稳定币有三种类型:基于法定货币抵押的稳定比,如:USDT、USDC。基于加密资产超额抵押的稳定比,如:DAI。基于算法的稳定币,如:AMPL *USDT的中心性,因为其代码包括对用户账户拉入黑名单,甚至进一步进行销毁账户持有USDT的操作 DAI是一种稳定币,能与美元(货币)进行等值兑换, 通过抵押数字资产发行,能与美元(货币)进行等值兑换。1$ = 1DAI。 相关概念SCD(Single-Collateral DAI, SCD):SCD是单担保DAI,早期DAI系统只支持eth作为担保资产,现在已经支持多担保DAI(MCD) MakerDAO :MakerDAO是Maker系统的去中心化自治组织,MakerDAO的治理者们使用MKR参与系统的维护和DAI的管理 Maker: Maker是以太坊上的智能合约平台,能够通过CDP、自动化反馈机制和适当的外部激励手段支持并稳定DAI价格 MRK是Maker协议治理代币,MKR的持有者通过执行投票(Executive Voting)和治理投票(Governance Polling)对Maker系统进行治理。MKR的持有者还可以通过Maker的运营中获得利润 投票会锁定MRK代币,一票等于一个MKR代币(MKR持有者可以将MKR存入投票合约以获得投票权重) Maker Vault是合约用于质押资产生成DAI代币的智能合约机制,DAI用户可以通过向中介(交易所)购买或质押资产得到DAI CDP(Collateralized Debt Positions, CDP)——抵债仓库: 保存用户所存储的抵债资产,并允许用户生成DAI,一旦生成DAI就代表生成例一笔债务。 CDP会锁定资产,知道用户偿还DAI后才可以拿回资产。CDP能够进行超额抵押,可完成像杠杆的金融操作 DSR(DAI Savings Rate,DSR):DAI 存款利率,通过将用户持有的DAI锁入Maker协议的DSR合约就可以获得额外的DAI收益 Maker协议DAI也是加密货币的一种,用户可以使用ETH或Bitcoin抵押兑换DAI代币。为了保证1 DAI能兑换1 $,DAI设计者对DAI设计了一系列机制来确保DAI的稳定 Vault——Maker抵押仓库过程: 用户向Maker发送交易创建CDP,然后充值一定数量的抵押品(ETH, Bitcoin) Vault所有者可以生成一笔交易,在交易中通过调整CPD(中的抵押率)选择需要生成的DAI数量 取回自己的抵押品,需要使用 DAI(赎回费用)+ Stability Fee(手续费),手续费需要使用DAI支付 用户支付了一定DAI后,用户就能发送一个交易给MKR并取回所有抵押资产 DAI的抵押案例 Maker清算机制1.当ETH升值时 \t用户能够获得更多抵押DAI 2.当ETH贬值时 在DAI中会有keeper对市场进行监控,如果抵押率超过的安全区就会触发清算 用户CPD设置为300%,DAI市场安全区在150%,当CPD小于150%就会触发清算 \t用户面临清算会以下面三个步骤进行: Collateral Auction(抵押品拍卖) Debt Auction(债务拍卖) Surplus Auction(盈余拍卖) Maker协议会根据清算率去对用户的vault当前的价值进行判断。每个类型的vault都有各自的清算率,MKR持有者可以通过投票来进行调整 面临清算前有两种方式可以避免:一是可以抵押更多的ETH,二是偿还部分DAI的债务 Collateral Auction当用户面临清算时不采取任何措施就会触发Collateral Auction。该机制会将用户的质押品进行拍卖(通过Maker的拍卖机制),拍卖得到的DAI会用于偿还债务。 如果拍卖得到的DAI多于清算罚金(Penalty Fee),还会触发反向担保品竞拍(Reverse Collateral Auction),将除去清算罚金后的DAI返还给用户。如果拍卖得到的DAI不足以偿还,债务就会由Maker解决 拍卖获取的DAI会立马用于偿还债务,这部分DAI会被销毁 Debt Auction在拍卖得到的DAI不足以偿还债务时,亏损会由Maker承担。Maker会有一个专门用于处理该亏损的缓冲金池子(Maker Buffer)。 如果Maker Buffer没有足够的DAI,Maker协议会触发Debt Auction。 Debt Auction会mint新的MKR代币,这些MKR通过DAI作为货币进行拍卖。拍卖得到的DAI会流入Maker Buffer Surplus Auction竞拍和稳定费的DAI都会流入Maker buffer中,Maker buffer会有一个上限。当Maker Buffer的DAI超过这个上限,就会触发Surplus Auction 。 即,MKR持有者可以使用MRK去竞拍固定数量的DAI。当Surplus Auction结束时,Maker协议会销毁掉拍卖所得的MRK从而减少MRK的供应量 MakerDAO Maker协议的外部参与者Maker协议的运作需要依靠各种外部参与者:Keeper、Oracle、Global Settler和Maker社区成员 Keeper——看护者keeper通常是独立的参与者,他们会在套利机制下为去中心化系统提供流动性。比如,在DAI价格低于1$时,他们会将其买入;在DAI价格高于1$时,他们会将其卖出。Keeper的行为会帮助DAI维持在1$左右。Keeper也会参与Collateral Auction、Debt Auction、Surplus Auction Oracle——预言机Maker协议需要实时地了解到市场上的担保物价值,从而能够知道何时触发清算机制。Oracle就是能够提供实时的信息的对象。 Maker 协议的内部担保物价格来自去中心化信息输入架构(decentralized Oracle infrastructure) 。该架构由大量名为 “喂价机(Oracle Feed)” 的独立节点组成。MKR 投票者选出一组可信赖的喂价节点,并由通过以太坊交易向 Maker 系统提供价格信息。群组的节点数量也是由 MKR 投票者来控制的。 Global Settler——全局清算者Global Settler也叫Emergency Oracle。他是由MRK持有者投票选举出来的,是为了在Maker协议遭到破坏(治理流程或Oracle被攻击)而导致重大损失的策略。 Global Settler拥有冻结单个Oracle,触发紧急关停机制(Emergency Shutdown)的权力。 MKR治理代币Maker协议是双币模型,协议使用DAI作为稳定代币,MKR作为Maker的治理代币。 MKR holder能够参与投票去修改Maker协议。Maker协议面临一些紧急情况时,这些holder能触发关停机制。 MKR 持有者可以对以下事项进行票决: 引入新的担保物类型,并为其设置一组风险参数 修改、乃至增加一种或多种现有担保物资产类型的风险参数 修改 DAI 存款利率 选出喂价机节点群组 选出紧急信息输入者群组 触发紧急关停 升级系统 对MKR holder的激励——Maker协议产生的部分收入会用于奖励MKR holder,这些奖励也会影响他们做出对Maker协议有利的决策 MakerDAO的盈利来源和其分配为了了解MakerDAO的来源,我们必须要清楚以下参数 稳定费(Stability Fee): 稳定费是根据一个金库所生成的 DAI 数量来计算的年利息(对生成 DAI 的用户来说,稳定费率相当于贷款的年化利率 清算罚金(Liquidation Penalty):清算罚金是当清算发生时,根据金库中未偿还 DAI 的总量向用户收取的额外一笔费用 Maker的盈利收入 Vault生成的DAI所需的稳定费 当用户通过不同的Token生成Vault以获得DAI,都会有一笔Stability Fee,这笔Fee会在vault关闭或清算时进行结算 拍卖清算获得的清算罚金 在用户vault清算后,需要向maker系统支付一笔Liquidation Penalty + Stability Fee RWAs(Real World Asserts),购买现实世界的资产以获取收益;如,美国国债等 RWAs现在成为了Maker利息的主要收入 让我用 Clydesdale 举个例子。本质上,Clydesdale 建立一个 SPV 并从 Maker 借入 DAI。然后此 DAI 通过 PSM 兑换为 USDC,并兑换成美元现金。这些美元现金然后用于购买国债,国债由 SPV 持有,作为借入 DAI 的抵押品。Clydesdale 从管理 SPV 中赚取少量报酬,剩余的美国国债利息归 Maker。 Maker的盈利分配Maker会将其盈利分配到 DSR,Maker协议的存款合约会将部分盈利支付存款利息 Keeper,参与清算的Keeper会获得部分激励,比如:参与清算启动拍卖(keeper调用dog .bark)的keeper会获得一笔tip + 拍卖抵押品部分DAI MKR holder :协议盈利也会分配给MKR持有者,协议会将超出协议缓冲金部分的DAI用于回购MKR并将其销毁 参考资料 Maker白皮书 梁培利的个人空间-梁培利个人主页-哔哩哔哩视频 (bilibili.com) https://www.bitget.fit/zh-CN/news/detail/12560603813192 Dai Stats","tags":["DAI","Maker","稳定币"],"categories":["区块链","MakerPotocol"]},{"title":"bot","path":"/wiki/index.html","content":"go-bot"}]