当面试官问你Uniswap的时候,你应该想到什么?

这篇文章主要介绍了当面试官问你Uniswap的时候,你应该想到什么? ,文中通过代码以及文档配合进行讲解,很详细,它对在座的每个人的研究和工作具有很经典的参考价值。 如果需要,让我们与区块链资料网一起学习。

https://www.interchains.cc/23164.html

当面试官问你Uniswap的时候,你应该想到什么?是很好的区块链资料,他说明了区块链当中的经典原理,可以给我们提供资料,当面试官问你Uniswap的时候,你应该想到什么?学习起来其实是很简单的,

不多的几个较为抽象的概念也很容易理解,之所以很多人感觉当面试官问你Uniswap的时候,你应该想到什么?比较复杂,一方面是因为大多数的文档没有做到由浅入深地讲解,概念上没有注意先后顺序,给读者的理解带来困难

当面试官问你Uniswap的时候,你应该想到什么?

  • samczsun
  • Uniswap

这段时间非常有幸跟行业内人士聊了一会,算是第一次正式接触到区块链blockchain行业内从业者,很激动。整体感觉这个行业充满了变化,挑战和机遇,它会给年轻人机会,目前我正在考虑转行该行业,希望能够在三年后回首自己的选择时能够不后悔,不遗憾。

这段时间非常有幸跟行业内人士聊了一会,算是第一次正式接触到区块链blockchain行业内从业者,很激动。整体感觉这个行业充满了变化,挑战和机遇,它会给年轻人机会,目前我正在考虑转行该行业,希望能够在三年后回首自己的选择时能够不后悔,不遗憾。

当面试官问你Uniswap的时候,你应该想到什么?

Uniswap的行业地位

uniswap是以太坊eth上的一个DAPP应用,后续的DEFI繁荣,出现各种各样的SWAP,其代码鼻祖就是Uniswap。

AMM

什么是AMM,价格如何移动?

AMM,全称Automated Market Makers,翻译过来是自动做市商。其基础模型来源于Vitalik于2017年发表的博客,讨论了“恒定乘机公式”,即每一个Uniswap Pair 中存有两种资产,并为这两种资产提供流动性。在为资产提供流动性时,保持两种Token储备的乘积不能减少的不变性。交易者须在交易中支付30个基点的费用,这些费用将用于流动性提供者。即保证$R_A * R_B geq K$

下面我们将分别结合swap,mint,burn来讨论AMM曲线的移动。

Swap

首先我们结合Uniswap中的简化版Swap代码来分析下价格移动

// 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 {         //拿到swap之前的tokenA和tokenB的余额         (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings         //拿到此时刻的tokenA和tokenB的余额             balance0 = IERC20(_token0).balanceOf(address(this));             balance1 = IERC20(_token1).balanceOf(address(this));         //乐观转账给address To             if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens             if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens         //通过余额的差值计算得到要交换的Token的数量         uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;         uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;         //扣除书续费,验证转账后的余额满足 X*Y >= K的要求         { // 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);     }

可以看到,如下图要从A点移动到B点,通过调用Uniswap的swap函数,需要先转账给pair合约一定数量($X_1-X_0$)的tokenB,然后pair合约将对应数量的tokenA转账出去到目标地址。则转装出去的tokenA的数量为$Y_0-Y_1$, 在忽略手续费的情况下,此时的B点应该要落在曲线上。 当面试官问你Uniswap的时候,你应该想到什么?

Mint

function mint(address to) external lock returns (uint liquidity) {         //拿到调用mint前的tokenA和tokenB的余额         (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings         //拿到此时刻的TokenA和TokenB的余额         uint balance0 = IERC20(token0).balanceOf(address(this));         uint balance1 = IERC20(token1).balanceOf(address(this));         //计算得到打进账户的两种Token的数量         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);         }         //发送LP Token         _mint(to, liquidity);         // 更新账户余额         _update(balance0, balance1, _reserve0, _reserve1);         if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date     }

可以看到,要让图中点A移动到点B,通过调用Uniswap中的Mint函数实现。如图中A点所示,其TokenX=2, TokenY=5000, 在保证$dy/dx$不变的情况下,即价格稳定的情况下,向Pair合约中,转账TokenX=1,TokenY=2500,此时系统的K值会由最初的10000增长为22500,点A移动到点B。即价格移动曲线也更新为最新的K=22500这条曲线。 当面试官问你Uniswap的时候,你应该想到什么?

Burn

// this low-level function should be called from a contract which performs important safety checks     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方法是Mint方法的逆向,先向Pair合约中,转账一定量的LP token,然后通过Pair合约向外转账tokenA, tokenB. 使得上图中的B点回落到A点。

数值精度

由于solidity中并不原生支持小数,Uniswap采用UQ112.112这种编码方式来存储价格信息。UQ112.112意味着该数值采取224位来编码值,前112位存放小数点前的值,其范围是$[0,2^{112}-1]$,后112位存放小数点后的值,其精度可以达到$frac{1}{2^{112}}$.

library UQ112x112 {     uint224 constant Q112 = 2**112;      // 编码:将一个uint112的值编码为uint224     //0000000000000000000000000000000000000000000000000000000000000001 => 0x01 uint112     //00000000 => 前32位留空     //0000000000000000000000000001 => 整数部分     //0000000000000000000000000000 => 小数部分     function encode(uint112 y) internal pure returns (uint224 z) {         z = uint224(y) * Q112; // never overflows     }     // divide a UQ112x112 by a uint112, returning a UQ112x112     function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {         z = x / uint224(y);     } }

Uniswap选择UQ112.112的原因:因为UQ112.112可以被存储位uint224,这会留下32位的空闲Storage插槽位。再一个是每个Pair合约的reserve0和reserve1都是Uint112,同样会留下32位的空闲插槽位。特别的是pair合约的reserve连同最近一个区块的时间戳一起存储时,该时间戳会对$2^{32}$取余数称为uint32格式的数值。加之,尽管在任意给定时刻的价格都是UQ112.112格式,保证为uint224位,一段时间累积的价格并不是该格式。在储存插槽中末端的额外的32位可以用来储存累积的价格的溢出部分。这种设计意味着在每一个区块的第一笔交易中只需要额外的三次SSTORE操作(目前的开销是15000gas)。

选择UQ112.112的缺点:最末端的32位用来储存时间戳会溢出,事实上一个Uint32的时间戳溢出点是02/07/2016.

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 上述三个参数刚好占据一个slot,一个slot 32位,上面三个参数编码后就是14+14+4=32位 该slot中的数据排列是: blockTime|Reserve1|Reserve0 //00000000 => blockTimestampLast //0000000000000000000000000000 => reserve1 //0000000000000000000000000000 => reserve0

为什么可以看到合约中存在x*1000/1000,目的是什么?

一方面是因为solidity中没有小数的概念,另一方面是为了保证数值精度

uint256 reward = (amountDeposited * 100) / totalDeposits; 与 uint256 rewardInWei = (amountDeposited * 100 * 10 ** 18) / totalDeposites;  如果amountDeposited=10000000, totalDeposits = 400+1000000: reward=99 如果amountDeposited = 100, totalDeposits = 400 + 1000000 reward = 0 ether rewardInWei = 99960015993602 wei = 0.00009996 ether

闪电兑

Uniswap V2添加了闪电兑功能,允许用户在付钱之前接受和使用Token资产,只要他们在同一笔原子交易中。闪电兑换功能在swap函数中实现:IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); 也可以写成 address(to).callWithSigature("uniswapV2Call(address,uint256,uint256,bytes)", msg.sender, amount0Out, amount1Out, data). 当回调函数完成后,合于会检查新的账户余额并确认常数K满足要求(扣除手续费后的常数K). 如果合约没有足够的余额,则会回退整笔交易。

用户也可以用同一种Token来偿还给Pair合约,而不是必须完成swap交易。这事实上是允许任何人闪电贷任何一种储存在Pair中的资产,只需要支付0.3%的手续费即可。

// 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 {         //拿到swap之前的tokenA和tokenB的余额         (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings         //拿到此时刻的tokenA和tokenB的余额             balance0 = IERC20(_token0).balanceOf(address(this));             balance1 = IERC20(_token1).balanceOf(address(this));         //乐观转账给address To             if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens             if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens         //闪电兑-执行用户定义的回调合约,在乐观转账给address To和确保K值不减之间时调用         if(data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);         //通过余额的差值计算得到要交换的Token的数量         uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;         uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;         //扣除书续费,验证转账后的余额满足 X*Y >= K的要求         { // 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);     }

LP TOKEN初始供应

要理解LP token的供应值,就需要理解什么是LP token。

在Uniswap中,LP token在mint中被铸造出来,在burn中销毁。除开首次铸造,非首次铸造的LP token数量都是与LP token的当前总量成线性关系。LP token的价值来源于流动性的提供,即mint方法。LP token的增值逻辑是时间段($t_1$,$t_2$)间的swap交易手续费的累计。swap的交易手续费又可以表现为时间段($t_1$,$t_2$)对应的($sqrt{k_1}$,$sqrt{k_2}$)的增值。

首次铸币

首次铸币时,如何确定LP token的初始供应的数量呢?

$$ s{minted} = sqrt{x{deposited}cdot y_{deposited}}=sqrt{k} $$

该公式确保了任意时刻的LP token的价值与初始供应的tokenA,tokenB的比例无关。例如,一个流动性提供者存入2 tokenA,200 tokenB,此时tokenA/tokenB的价格为100,则该流动性提供者会获得$sqrt{2cdot 200}=20$ LP token。该20份 LP token的价值就是2 tokenA, 200 tokenB, 以及此时间段间积累的手续费。如果一个流动性提供者最初存入的是2 tokenA, 800 tokenB, 此时tokenA/tokenB的价格为400,则该流动性提供者会获得$sqrt{2cdot 800}=40$ LP token。

非首次铸币

非首次铸币时,LP token的供应量为:

$$ s{minted}=frac{x{deposited}}{x{starting}}cdot s{starting} $$

上述公式保证了LP token的价值永远不会低于$sqrt{k}$.

然而,当我们对比mint部分代码时,会发现如下一行代码:

if (_totalSupply == 0) {     liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);    _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens }

为什么在首次铸币时,要向address(0) 传送一部分LPtoken呢?

因为根据上述公式,存在如下的一种攻击模式,称为LP token操纵

sequenceDiagram      opt 首次铸币     攻击者->>Pair合约: tokenA.transfer(pair,1 wei)     攻击者->>Pair合约: tokenB.transfer(pair,1 wei)     攻击者->>Pair合约: pair.mint(msg.sender)     Pair合约 ->> 攻击者: LP token=1 wei      end     opt 二次铸币     攻击者->>Pair合约: tokenA.transfer(pair,10**22 wei)     攻击者->>Pair合约: tokenB.transfer(pair,10**22 wei)     攻击者->>Pair合约: pair.mint(msg.sender)     Pair合约 ->> 攻击者: LP token=10**22 wei      end     opt SYNC 同步余额     攻击者->>Pair合约: pair.sync()     end     opt 三次铸币     受害者->>Pair合约: tokenA.transfer(pair,10**22 wei)     受害者->>Pair合约: tokenB.transfer(pair,10**22 wei)     受害者->>Pair合约: pair.mint(msg.sender)     Pair合约 ->> 受害者: LP token=? wei      end

平台手续费

Uniswap支持0.05%的平台手续费,它可以在Factory合约中设置开或者关。如果被设置为收取平台手续费,则该平台手续费会转账给feeTo地址(该地址是在工厂合约中设置)。

收取平台手续费的位点

如果设置了feeTo地址,Uniswap将会开始收取0.05%的平台手续费,其为流动性提供者收取的0.3%中的$frac1{6}$, 即交易者仍然只会支付0.3%的手续费,其中$frac5{6}$会支付给流动性提供者,剩余的$frac1{6}$会支付给feeTo地址。如果每一笔交易都将其手续费的1/6实时打给feeTo地址会极度消耗gas。为了避免这种情况,累计的手续费只会在提供流动性和移除流动性时计算。合约会计算累计的手续费,并铸造新的LP token给到feeTo地址,在任何token被铸造或者销毁前进行。

function mint(address to) external lock returns (uint liquidity) {         //拿到调用mint前的tokenA和tokenB的余额         //拿到此时刻的TokenA和TokenB的余额         //计算得到打进账户的两种Token的数量        // 在实际创建任何LPtoken之前,先计算mintFee,即先把平台手续费对应的LP token发放了。         bool feeOn = _mintFee(_reserve0, _reserve1);         if (_totalSupply == 0) {          //首次铸币         } else {             //非首次铸币         }         //发送LP Token         // 更新账户余额         if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date     } function _mintFee(uint112 resserve0, uint112 reserve1) private returns (bool feeOn) {         //拿到工厂合约设置的feeTo地址         address feeTo = IUniswapFactory(factory).feeTo();         //根据feeTo地址判断是否开启收取平台手续费         feeOn = (feeTo == address(0));         //如果收取平台手续费         if (feeOn) {             if (KLast == 0) {             //判断是不是首次铸币,首次铸币不收取平台手续费,因为此时没有swap,压根就没有手续费                 return;             } else {             //如果不是首次铸币,计算要发放的LP token的数量,转给feeTo地址                 // s1 = totalSupply, k1 = KLast, k2 = reserve0 * reserve1                 uint root_k1 = math.sqrt(KLast);                 uint root_k2 = math.sqrt(uint256(reserve0).mul(uint256(reserve1)));                 require(root_k2 > root_k1, "root_k2 <= root_k1");                 uint numerator = root_k2.sub(root_k1);                 uint denominator = root_k2.mul(5).add(root_k1);                 uint amount = numerator.div(denominator).mul(totalSupply);                 _mint(feeTo, amount);             }         } else if (KLast != 0) {         //如果不收取平台手续费,然而KLast不为0,则将其设置为0;                 KLast = 0;         }    }

平台手续费的数量

现在最大的问题是,如何计算发放给feeTo地址的LP token的数量。

首先要明确一点,手续费的收集体现在K值的增加上。由于每一笔swap都收取了0.3%的手续费,其会导致$sqrt{k}$值缓慢增加。对比$t_1$和$t_2$时刻的$sqrt{k_1}$和$sqrt{k_2}$, 其差值即为$t_2$与$t_1$时刻间的手续费,则在任意时间段($t_1$,$t_2$)间,其手续费占当前时刻$t_2$的比重为:

$$ f_{1,2}=frac{sqrt{k_2}-sqrt{k_1}}{sqrt{k_2}} $$

此时,我们假设$t_2$时刻的LPtoken的总供应量为$s_1$,需要发放给feeTo地址的LP token的量为$s_m$, 由于LP token不能减发,且发送给feeTo地址的LPtoken在发送给流动性提供者的LPtoken之前发放,故需要满足如下等式:

$$ frac{s_m}{s_m+s1} = phi times f{1,2} $$

其中$phi$为平台手续费所占所有手续费的比例,此处为1/6

由方程式1,2得到,此时的发放给feeTo地址的平台手续费为:

$$ s_m=frac{sqrt{k_2}-sqrt{k_1}}{(1/phi-1)cdot sqrt{k_2} + sqrt{k_1}}times s_1 $$

带入$phi=1/6$后得到:

$$ s_m=frac{sqrt{k_2}-sqrt{k_1}}{5cdot sqrt{k_2} + sqrt{k_1}}times s_1 $$

元交易

由Uniswap Pair合约中铸造出的LP token天然的支持元交易。这意味着用户可以通过签名的方式授权转移LP token, 而不是必须由用户地址发起的以太坊eth上的交易。任何人可以通过调用permit函数提交他们的签名,付给gas费用,并同时进行多笔交易。

// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; constructor() public {     uint chainId;     assembly {         chainId := chainid     }     DOMAIN_SEPARATOR = keccak256(         abi.encode(             keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),             keccak256(bytes(name)),             keccak256(bytes('1')),             chainId,             address(this)         )     ); } function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {     require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');     bytes32 digest = keccak256(         abi.encodePacked(             'x19x01',             DOMAIN_SEPARATOR,             keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))         )     );     address recoveredAddress = ecrecover(digest, v, r, s);     require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');     _approve(owner, spender, value); }

要理解Uniswap V2中提到的元交易部分,就需要理解EIP-712

EIP-712 动机

旨在提高链外消息签名的可用性,以便在链上使用。我们看到越来越多的采用链外消息签名,因为它节省了gas,减少了区块链blockchain上的交易数量。当前签名的消息是向用户显示的不透明的hex字符串,与构成消息的项目几乎没有上下文。 当面试官问你Uniswap的时候,你应该想到什么?

EIP-712确定了编码数据及其结构,允许它显示给用户在签名时进行验证。以下是用户在签署 EIP712 消息时可以显示的内容的示例。 当面试官问你Uniswap的时候,你应该想到什么?

EIP-712 在EIP-191的基础上,进一步提出了一种结构化的签名信息,用于在前端(线下)签名时展示相应的信息,而不是一串难以理解的hex串。其在设计时,主要考虑到两方面特性:1.确保可确认性。如果不是确定性的,则哈希可能从签名到验证的瞬间而有所不同,导致签名被错误地拒绝 2. 可注射性。如果它不是注射的,那么我们的输入集中有两个不同的元素,它们对相同的值进行哈希值,导致签名对不同的不相关消息有效。

EIP-712 结构化签名信息

$$ mathtt{T}cupmathtt{B^{8n}}cupmathtt{S} $$

  • 对于交易数据$T$, 其编码方式仍然遵循RLP编码方式
  • 对于bytes数据$B^{8n}$, 其编码方式遵循EIP-191, 即

    $$ encode(B^{8n})=x19Ethereum Signed Message:n || len(message) || message $$

    其中len(message)是没有0开头的Ascii编码值

  • 对于结构化数据$S$, 其编码方式遵循EIP-712, 即

    $$ encode(domainSeparator,message)=x19x01 || domainSeparator || hashStruct(message) $$

DomainSeparator域定义

domainSeparator是一个结构体,名为EIP712Domain:

struct EIP712Domain{     string name, //用户可读的域名,如DAPP的名字     string version, // 目前签名的域的版本号     uint256 chainId, // EIP-155中定义的chain ID, 如以太坊eth主网为1     address verifyingContract, // 用于验证签名的合约地址     bytes32 salt // 随机数 }

该结构体必须按照如上定义的字段和顺序定义,且如果有不需要的字段,可以跳过,但是不能改变该结构体内部顺序。

则对于DomainSeparator的编码方式应遵循如下编码方式:

$$ hashStruct(S)=keccak256(typeHash || encodeData(S)) typeHash=keccak256(encodeType(typeOf(S))) $$

对于上述结构体EIP712Domain来讲:

encodeType编码

encodeType编码方式与函数选择器的编码方式不同,其包含了变量名和空格。

$$ encodeType=EIP712Domain(string name,string version,uint256 chainId,address verifyingContract) $$

对于encodeType编码,如果结构体中包含结构体,则只需要按照字母顺序将结构体依次编码即可

如针对结构体:

struct Transaction{     Person from,     Person to,     Asset tx } struct Asset{     address token,     uint256 amount } struct Person{     address wallet,     string name }

编码后的encodeType为:

Transaction(Person from,Person to,Asset tx)Asset(address token,uint256 amount)Person(address wallet,string name)

encodeData编码

encodeData编码要求所编码的每一位都占据32bytes,且编码的顺序要与encodeType中定义的顺序一致。针对string 或者 bytes 等动态类型,即长度不定的类型,其取值为 keccak256(string) 即内容的hash值。针对固定长度的类型,如bool 值其编码为Uint256, address 编码为uint160, bytes1~bytes32 编码为左对齐的bytes32,右侧补零。

故针对DomainSepeartor,其编码值为:因为每一项必须是32bytes,所以用abi.encode,而不用abi.encodePacked

abi.encode(     keccak(bytes(name)),     keccak(bytes(version)),     uint256(chianid),     uint256(uint160(verifyingContract)) );

结合起来,故知domainSeperator的值应为:

DOMAIN_SEPARATOR = keccak256(     abi.encode(         keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), // encodeType         keccak256(bytes(name)), // encodeData-name         keccak256(bytes("1")), // encodeData-version         chianId, // encodeData-chainId         uint256(address(this)) // encodeData-address     ) );

结构化数据编码

$$ encode(domainSeparator,message)=x19x01 || domainSeparator || hashStruct(message) $$

根据上述公式,在uniswap中,我们需要明确需要编码的struct,即用户调用时显示在前端off-chain部分的信息:

struct Permit{     address owner,     address spender,     uint256 value,     uint256 nonce,     uint256 deadline }

则该Permit的encodeType为:

encodeType(Permit) = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")

则该Permit的encodeData为:因为每一项都必须是32bytes,所以使用abi.encode, 而不是用abi.encodePacked

abi.encode(     uint256(uint160(owner)),     uint256(uint160(spender)),     value,     nonce,     deadline )

故该Permit的结构化签名信息应为:

bytes memory mssage = abi.encodePacked(     x19x01,     DOMAIN_SEPERATOR,     keccak256(         abi.encode(             keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),             uint256(uint160(owner)),             uint256(uint160(spender)),             value,             nonces[owner]++,             deadline         )) )

结合Uniswap中的元数据定义看,在Permit函数中,对上述消息Hash后,然后通过ecrecover方法来验证address地址。当验证无误时,执行_approve方法。

确定PAIR合约地址

uniswap V2在工厂合约中,使用了create2关键字来创建合约。下面我们结合黄皮书,来进一步学习下create2与create

$$ begin{eqnarray} a & equiv & A(s, boldsymbol{sigma}[s]{mathrm{n}} – 1, zeta, mathbf{i}) label{eq:new-address} A(s, n, zeta, mathbf{i}) & equiv & mathcal{B}{96..255}Big(mathtt{KEC}big(B(s, n, zeta, mathbf{i})big)Big) B(s, n, zeta, mathbf{i}) & equiv & begin{cases} mathtt{RLP}big(;(s, n);big) & text{if} zeta = varnothing (255) cdot s cdot zeta cdot mathtt{KEC}(mathbf{i}) & text{otherwise} end{cases} end{eqnarray} $$

create

对于create关键字,其随机数$zeta=varnothing$, 则新生成的合约地址仅和msg.sendernonce相关。其具体地址的值为经过RLP编码后的msg.senderNonce 的bytes,然后经过哈希后的最右侧160位。

如下:

msg.sender = 0xfefefefefefefefefefefefefefefefefefefefe nonce = 1 (msg.sender, nonce) = [0xfefefefefefefefefefefefefefefefefefefefe,1] RLP((sender,nonce)): 此为list,需按照list进行RLP编码 首先是msg.sender 的RLP编码: 0x94fefefefefefefefefefefefefefefefefefefefe (0x80+0x14) 然后是nonce 的RLP编码:01 所以(sender,nonce) 的RLP编码是: 0xD694fefefefefefefefefefefefefefefefefefefefe01 (0xc0+0x16) 则Keccak256(RLP((sender,nonce)))= 0x1eee6eebe7cda42d60b04fbbf862acff87608602aa2cb646f5d67560f5c3e9d7 则生成的地址为:0xf862acff87608602aa2cb646f5d67560f5c3e9d7 右侧160位

create2

对于create2关键字,其随机数$zeta neq varnothing$, 在uniswap v2中,该随机数定义为:$zeta= keccak256(abi.encodePacked(token0, token1));$ 则create2地址应为:

bytes32 memory salt = keccak256(abi.encodePacked(token0,token1)); bytes memory init_code = type(UniswapV2Pair).creationCode; B = abi.encodePacked(     hex'ff',     address(factory),     salt,     keccak256(init_code) ); address pair = address(uint160(keccak256(B)));

那么为什么要选用create2这一opcode,而不是create呢?

原因在于create生成的地址与nonce相关,即与pair合约生成的顺序相关。我们希望pair合约的地址与该顺寻无关,故选用create2这一opcode。

预言机

整个Uniswap V2的预言机部分代码仅有4-5行,然而其却实现了时间平均价格,取代容易被操纵的瞬时价格,成为一个更加稳定的价格预言机。

下面我们将结合白皮书和代码部分来分析Uniswap V2中,该预言机是如何设计以及为什么需要这样设计

首先是价格,某时刻t的资产价格a/b 为此时刻的reserve_tokenA 与 reserve_tokenB的比值

$$ pt=frac{r{t}^{a}}{r_t^{b}} $$

瞬时价格

如果用瞬时价格作为预言机会出现什么问题?

瞬时价格容易被操纵,可以通过第一笔交易swap,让A/B的资产价格瞬时下跌,然后再第二笔交易中,利用基于此瞬时价格作为预言机的合约,比如一些清算服务等,去进行清算等,第三笔交易再将价格拉回原位。甚至此三笔交易可以在一个原子交易中完成。 当面试官问你Uniswap的时候,你应该想到什么?

三明治攻击

当面试官问你Uniswap的时候,你应该想到什么?

时间加权平均价格

uniswap v2通过测量和记录每一个区块最开始的第一笔交易的价格。相较于同一个区块内的价格,该价格非常难以操纵。具体来说,Uniswap v2通过记录每个区块中有人与合约互动时的累积价格总和,来累积这个价格。

每个价格的权重是自上一个区块更新以来所经过的时间。这意味着,在任何给定的时间,累积器的值(在被更新后)都应该被加权。这意味着在任何特定时间(更新后),累积器的值应该是合约历史上每一秒钟的现货价格的总和。

$$ at=sum{i=1}^{t}{p_i} $$

为了估计从时间$t_1$到$t_2$的时间加权平均价格,一个外部调用者可以在$t_1$时检查累积器的值,然后在$t_2$时再次检查,将该值减去第一个值,然后再除以所经过的秒数。(请注意合约本身并不存储这个累积器的历史值–调用者必须在周期开始时调用合约来读取和存储这个值)

$$ p_{t_1,t2}=frac{sum{i=t_1}^{t_2}{p_i}}{t_2-t1}=frac{sum{i=1}^{t_2}pi-sum{i=1}^{t_1}{p_i}}{t_2-t1}=frac{a{t2}-a{t_1}}{t_2-t_1} $$

下面我们结合代码来看下具体如何实现:

uint public price0CumulativeLast; uint public price1CumulativeLast; // 只在每个区块的最开始的第一笔交易处记录price,并且将价格按照时间权重进行累加得到a_t function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {     // 判断是否是该区块的第一笔交易,只要该block的timestamp与上一次记录的Block.timestamp不一样,就说明不是同一个块     uint32 timestamp = uint32(uint(block.timestamp)%(2**32));     if (timestamp > prev_timestamp) {         //如果是,计算出瞬时价格pi         //uint256 priceAtoB = uint256(_reserve0).div(uint256(_reserve1));         uint256 priceAtoB = uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)); // 得到的price是uint224位编码,头4位留空给溢出用,前112位为正数部分,后112位为小数部分         uint256 priceBtoA = uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1));         //按照时间权重累加到a_t上         price0CumulativeLast += (timestamp-prev_timestamp).mul(priceBtoA);         price1CumulativeLast += (timestamp-prev_timestamp).mul(priceAtoB);     }     prev_timestamp = timestamp; }

最后得到的price1CumulativeLast虽然是uint256,但其实实质上仍然是UQ112.112编码的数值。前4位留空给溢出,前112位为正数,后112位为小数。 当面试官问你Uniswap的时候,你应该想到什么?

部分转自网络,侵权联系删除www.interchains.cchttps://www.interchains.cc/23164.html

区块链毕设网(www.interchains.cc)全网最靠谱的原创区块链毕设代做网站 部分资料来自网络,侵权联系删除! 最全最大的区块链源码站 ! QQ3039046426
区块链知识分享网, 以太坊dapp资源网, 区块链教程, fabric教程下载, 区块链书籍下载, 区块链资料下载, 区块链视频教程下载, 区块链基础教程, 区块链入门教程, 区块链资源 » 当面试官问你Uniswap的时候,你应该想到什么?

提供最优质的资源集合

立即查看 了解详情