diff --git a/0xRory.md b/0xRory.md index 81dc331c..d85e00e9 100644 --- a/0xRory.md +++ b/0xRory.md @@ -157,6 +157,15 @@ timezone: Asia/Taipei [WTF Academy Solidity 103 54 跨鏈橋 Note](/content/0xRory/120-跨鏈橋/跨鏈橋.md) - [WTF Academy Solidity 103 55 MultiCall Note](/content/0xRory/120-Multicall/Multicall.md) + [WTF Academy Solidity 103 55 MultiCall Note](/content/0xRory/120-MultiCall/MultiCall.md) + +### 2024.10.15 + + day23: + + [WTF Academy Solidity 103 56 去中心化交易所 Note](/content/0xRory/120-去中心化交易所/DEX.md) + + [WTF Academy Solidity 103 57 Flashloan Note](/content/0xRory/120-Flashloan/Flashloan.md) + diff --git a/content/0xRory/120-Flashloan/1.png b/content/0xRory/120-Flashloan/1.png new file mode 100644 index 00000000..12f5d400 Binary files /dev/null and b/content/0xRory/120-Flashloan/1.png differ diff --git a/content/0xRory/120-Flashloan/Flashloan.md b/content/0xRory/120-Flashloan/Flashloan.md new file mode 100644 index 00000000..a546efef --- /dev/null +++ b/content/0xRory/120-Flashloan/Flashloan.md @@ -0,0 +1,266 @@ +### 57. 閃電貸 + +「閃電貸攻擊」這個詞大家一定聽過,但是什麼是閃電貸?如何寫閃電貸合約?這一講,我們將介紹區塊鏈中的閃電貸。 + +閃電貸(Flashloan)是DeFi的一種創新,它允許用戶在一個交易中藉出並迅速歸還資金,而無需提供任何抵押。 + +想像一下,你突然在市場中發現了一個套利機會,但需要準備100萬u的資金才能完成套利。 + +在Web2,你去銀行申請貸款,需要審批,很可能錯過套利的機會。另外,如果套利失敗,你不光要支付利息,還需要歸還損失的本金。 + +而在Web3,你可以在DeFI平台(Uniswap,AAVE,Dodo)中進行閃電貸獲取資金,就可以在無擔保的情況下借100萬u的代幣,執行鏈上套利,最後再歸還貸款和利息。 + +閃電貸利用了以太坊交易的原子性: + +一個交易(包括其中的所有操作)要么完全執行,要么完全不執行。 + +如果一個用戶嘗試使用閃電貸並在同一個交易中沒有歸還資金,那麼整個交易都會失敗並被回滾,就像它從未發生過一樣。因此,DeFi平台不需要擔心借款人還不上款,因為還不上的話就意味著錢沒借出去; + +同時,借款人也不用擔心套利不成功,因為套利不成功的話就還不上款,也就代表借錢沒成功。 + + +![alt text](image.png) + +首先,要理解閃電貸會需要知道他就是在一筆交易內完成 借貸,操作,所以如果可以在短時間內到可以支付fee 的費用,與所借的資金,那麼就可以進行閃電貸。 + +``` +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +// UniswapV2闪电贷回调接口 +interface IUniswapV2Callee { + function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external; +} + +// UniswapV2闪电贷合约 +contract UniswapV2Flashloan is IUniswapV2Callee { + address private constant UNISWAP_V2_FACTORY = + 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + IUniswapV2Factory private constant factory = IUniswapV2Factory(UNISWAP_V2_FACTORY); + + IERC20 private constant weth = IERC20(WETH); + + IUniswapV2Pair private immutable pair; + + constructor() { + pair = IUniswapV2Pair(factory.getPair(DAI, WETH)); + } + + // 闪电贷函数 + function flashloan(uint wethAmount) external { + // calldata长度大于1才能触发闪电贷回调函数 + bytes memory data = abi.encode(WETH, wethAmount); + + // amount0Out是要借的DAI, amount1Out是要借的WETH + pair.swap(0, wethAmount, address(this), data); + } + + // 闪电贷回调函数,只能被 DAI/WETH pair 合约调用 + function uniswapV2Call( + address sender, + uint amount0, + uint amount1, + bytes calldata data + ) external { + // 确认调用的是 DAI/WETH pair 合约 + address token0 = IUniswapV2Pair(msg.sender).token0(); // 获取token0地址 + address token1 = IUniswapV2Pair(msg.sender).token1(); // 获取token1地址 + assert(msg.sender == factory.getPair(token0, token1)); // ensure that msg.sender is a V2 pair + + // 解码calldata + (address tokenBorrow, uint256 wethAmount) = abi.decode(data, (address, uint256)); + + // flashloan 逻辑,这里省略 + require(tokenBorrow == WETH, "token borrow != WETH"); + + // 计算flashloan费用 + // fee / (amount + fee) = 3/1000 + // 向上取整 + uint fee = (amount1 * 3) / 997 + 1; + uint amountToRepay = amount1 + fee; + + // 归还闪电贷 + weth.transfer(address(pair), amountToRepay); + } +} +``` + +- 计算闪电贷费用: + - Uniswap V2 收取借贷金额的 0.3% 作为费用。 + - 公式:fee = (amount1 * 3) / 997 + 1,并向上取整。 + - 计算需要偿还的总金额:amountToRepay = amount1 + fee。 + +- 归还闪电贷 + weth.transfer(address(pair), amountToRepay); + +#### 2. Uniswap V3闪电贷 +与Uniswap V2在swap()交换函数中间接支持闪电贷不同,Uniswap V3在Pool池合约中加入了flash()函数直接支持闪电贷。 + +``` +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +// UniswapV3闪电贷回调接口 +// 需要实现并重写uniswapV3FlashCallback()函数 +interface IUniswapV3FlashCallback { + /// 在实现中,你必须偿还池中由 flash 发送的代币及计算出的费用金额。 + /// 调用此方法的合约必须经由官方 UniswapV3Factory 部署的 UniswapV3Pool 检查。 + /// @param fee0 闪电贷结束时,应支付给池的 token0 的费用金额 + /// @param fee1 闪电贷结束时,应支付给池的 token1 的费用金额 + /// @param data 通过 IUniswapV3PoolActions#flash 调用由调用者传递的任何数据 + function uniswapV3FlashCallback( + uint256 fee0, + uint256 fee1, + bytes calldata data + ) external; +} + +// UniswapV3闪电贷合约 +contract UniswapV3Flashloan is IUniswapV3FlashCallback { + address private constant UNISWAP_V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + uint24 private constant poolFee = 3000; + + IERC20 private constant weth = IERC20(WETH); + IUniswapV3Pool private immutable pool; + + constructor() { + pool = IUniswapV3Pool(getPool(DAI, WETH, poolFee)); + } + + function getPool( + address _token0, + address _token1, + uint24 _fee + ) public pure returns (address) { + PoolAddress.PoolKey memory poolKey = PoolAddress.getPoolKey( + _token0, + _token1, + _fee + ); + return PoolAddress.computeAddress(UNISWAP_V3_FACTORY, poolKey); + } + + // 闪电贷函数 + function flashloan(uint wethAmount) external { + bytes memory data = abi.encode(WETH, wethAmount); + IUniswapV3Pool(pool).flash(address(this), 0, wethAmount, data); + } + + // 闪电贷回调函数,只能被 DAI/WETH pair 合约调用 + function uniswapV3FlashCallback( + uint fee0, + uint fee1, + bytes calldata data + ) external { + // 确认调用的是 DAI/WETH pair 合约 + require(msg.sender == address(pool), "not authorized"); + + // 解码calldata + (address tokenBorrow, uint256 wethAmount) = abi.decode(data, (address, uint256)); + + // flashloan 逻辑,这里省略 + require(tokenBorrow == WETH, "token borrow != WETH"); + + // 归还闪电贷 + weth.transfer(address(pool), wethAmount + fee1); + } +} +``` +Uniswap V3每笔闪电贷的手续费与交易手续费一致。 + +#### 3. AAVE V3闪电贷 + + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +interface IFlashLoanSimpleReceiver { + /** + * @notice 在接收闪电借款资产后执行操作 + * @dev 确保合约能够归还债务 + 额外费用,例如,具有 + * 足够的资金来偿还,并已批准 Pool 提取总金额 + * @param asset 闪电借款资产的地址 + * @param amount 闪电借款资产的数量 + * @param premium 闪电借款资产的费用 + * @param initiator 发起闪电贷款的地址 + * @param params 初始化闪电贷款时传递的字节编码参数 + * @return 如果操作的执行成功则返回 True,否则返回 False + */ + function executeOperation( + address asset, + uint256 amount, + uint256 premium, + address initiator, + bytes calldata params + ) external returns (bool); +} + +// AAVE V3闪电贷合约 +contract AaveV3Flashloan { + address private constant AAVE_V3_POOL = + 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2; + + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + ILendingPool public aave; + + constructor() { + aave = ILendingPool(AAVE_V3_POOL); + } + + // 闪电贷函数 + function flashloan(uint256 wethAmount) external { + aave.flashLoanSimple(address(this), WETH, wethAmount, "", 0); + } + + // 闪电贷回调函数,只能被 pool 合约调用 + function executeOperation(address asset, uint256 amount, uint256 premium, address initiator, bytes calldata) + external + returns (bool) + { + // 确认调用的是 DAI/WETH pair 合约 + require(msg.sender == AAVE_V3_POOL, "not authorized"); + // 确认闪电贷发起者是本合约 + require(initiator == address(this), "invalid initiator"); + + // flashloan 逻辑,这里省略 + + // 计算flashloan费用 + // fee = 5/1000 * amount + uint fee = (amount * 5) / 10000 + 1; + uint amountToRepay = amount + fee; + + // 归还闪电贷 + IERC20(WETH).approve(AAVE_V3_POOL, amountToRepay); + + return true; + } +} +``` + +- 计算闪电贷费用: + - AAVE V3 收取借贷金额的 0.5% 作为费用。 + - 公式:fee = (amount * 5) / 10000 + 1。 + - 计算需要偿还的总金额:amountToRepay = amount + fee。 + +- 归还闪电贷 + IERC20(WETH).approve(AAVE_V3_POOL, amountToRepay); + + + +閃電貸很創新,但我感覺就是包裝起來可以一次性借貸的服務,只是用合約實現真的是很屌,感覺也不是對程式的理解而是對金融的理解,這樣的合約也是很有價值的。 之後還是要複習一下。 + diff --git "a/content/0xRory/120-\345\216\273\344\270\255\345\277\203\345\214\226\344\272\244\346\230\223\346\211\200/DEX.md" "b/content/0xRory/120-\345\216\273\344\270\255\345\277\203\345\214\226\344\272\244\346\230\223\346\211\200/DEX.md" new file mode 100644 index 00000000..78cc0d35 --- /dev/null +++ "b/content/0xRory/120-\345\216\273\344\270\255\345\277\203\345\214\226\344\272\244\346\230\223\346\211\200/DEX.md" @@ -0,0 +1,377 @@ +### 56. 去中心化交易所 + +將介紹恆定乘積自動做市商(Constant Product Automated Market Maker, CPAMM),它是去中心化交易所的核心機制,被Uniswap,PancakeSwap等一系列DEX採用。教學合約由Uniswap-v2合約簡化而來,包括了CPAMM最核心的功能。 + +#### 自動做市商 + +自動做市商(Automated Market Maker,簡稱AMM)是一種演算法,或者說是一種在區塊鏈上運行的智慧合約,它允許數位資產之間的去中心化交易。 + +AMM 的引入開創了一種全新的交易方式,無需傳統的買家和賣家進行訂單匹配,而是透過一種預設的數學公式(例如,常數乘積公式)創建一個流動性池,使得用戶可以隨時進行交易。 + +#### 恆定總和自動做市商 + +恆定總和自動做市商(Constant Sum Automated Market Maker, CSAMM)是最簡單的自動做市商模型,我們從它開始。它在交易時的約束為: + +k=x+y + +其中 +k為常數。也就是說,在交易前後市場中可樂和美元數量的總和保持不變。舉個例子,市場中流動性有10 瓶可樂和10 美元,此時 + +k=20,可樂的價格為1 美元/瓶。我很渴,想拿出2 美元來換可樂。交易後市場中的美元總量變為12,根據約束 + +k=20,交易後市場中有8 瓶可樂,價格為1 美元/瓶。我在交易中得到了2 瓶可樂,價格為1 美元/瓶。 + +`CSAMM` 的優點是可以確保代幣的相對價格不變,這點在穩定幣兌換中很重要,大家都希望1 USDT 總是能兌換出1 USDC。但它的缺點也很明顯,它的流動性很容易耗盡:我只需要10 美元,就可以把市場上可樂的流動性耗盡,其他想喝可樂的用戶就沒辦法交易了。 + +以下我們介紹擁有」無限「流動性的恆定乘積自動做市商。 + +#### 恆定乘積自動 +恆定乘積自動做市商(CPAMM)是最受歡迎的自動做市商模型,最早被Uniswap 採用。它在交易時的約束為: + +k=x∗y + +其中 + +k為常數。也就是說,在交易前後市場中可樂和美元數量的乘積保持不變。同樣的例子,市場中流動性有10 瓶可樂和10 美元,此時 + +k=100,可樂的價格為1 美元/瓶。我很渴,想拿出10 美元來換可樂。如果在CSAMM 中,我的交易會換來10 瓶可樂,並耗盡市場上可樂的流動性。但在CPAMM 中,交易後市場中的美元總量變為20,根據約束 + +k=100,交易後市場中有5 瓶可樂,價格為 + +20/5=4美元/瓶。我在交易中得到了5 瓶可樂,價格為 + +10/5=2美元/瓶。 + +CPAMM 的優點是擁有「無限」流動性:代幣的相對價格會隨著買賣而變化,越稀缺的代幣相對價格會越高,避免流動性被耗盡。上面的例子中,交易讓可樂從1 美元/瓶上漲到4 美元/瓶,以避免市場上的可樂被買斷。 + +下面,讓我們建立一個基於CPAMM 的極簡的去中心化交易所。 + + +#### 去中心化交易所合約 +下面,我們用智能合約寫一個去中心化交易所SimpleSwap,支援用戶交易一對代幣。 + +SimpleSwap繼承了ERC20 代幣標準,方便記錄流動性提供者提供的流動性。在構造器中,我們指定一對代幣地址token0和token1,交易所僅支援這對代幣。reserve0和reserve1記錄了合約中代幣的儲備量。 + +```solidity + +contract SimpleSwap is ERC20 { + // 代币合约 + IERC20 public token0; + IERC20 public token1; + + // 代币储备量 + uint public reserve0; + uint public reserve1; + + // 构造器,初始化代币地址 + constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") { + token0 = _token0; + token1 = _token1; + } +} +``` + +交易所主要有兩類參與者: +`流動性提供者(Liquidity Provider,LP)` +`交易者(Trader)`。 + + +- 流動性提供 + +流動性提供者給市場流動性,讓交易者獲得更好的報價和流動性,並收取一定費用 + +因為SimpleSwap合約繼承了ERC20 代幣標準,在計算好LP份額後,可以將份額以代幣形式鑄造給用戶。 + +下面的`addLiquidity()`函數實作了添加流動性的功能,主要步驟如下: + +- 將用戶新增的代幣轉入合約,需要用戶事先給合約授權。 +- 根據公式計算添加的流動性份額,並檢查鑄造的LP數量。 +- 更新合約的代幣儲備量。 +- 給流動性提供者鑄造LP代幣。 +- 釋放Mint事件。 + +```solidity +event Mint(address indexed sender, uint amount0, uint amount1); + +// 添加流动性,转进代币,铸造LP +// @param amount0Desired 添加的token0数量 +// @param amount1Desired 添加的token1数量 +function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){ + // 将添加的流动性转入Swap合约,需事先给Swap合约授权 + token0.transferFrom(msg.sender, address(this), amount0Desired); + token1.transferFrom(msg.sender, address(this), amount1Desired); + // 计算添加的流动性 + uint _totalSupply = totalSupply(); + if (_totalSupply == 0) { + // 如果是第一次添加流动性,铸造 L = sqrt(x * y) 单位的LP(流动性提供者)代币 + liquidity = sqrt(amount0Desired * amount1Desired); + } else { + // 如果不是第一次添加流动性,按添加代币的数量比例铸造LP,取两个代币更小的那个比例 + liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1); + } + + // 检查铸造的LP数量 + require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED'); + + // 更新储备量 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + // 给流动性提供者铸造LP代币,代表他们提供的流动性 + _mint(msg.sender, liquidity); + + emit Mint(msg.sender, amount0Desired, amount1Desired); +} +``` + +- 下面的`removeLiquidity()`函數實現移除流動性的功能,主要步驟如下: + + - 取得合約中的代幣餘額。 + - 以LP的比例計算要轉出的代幣數量。 + - 檢查代幣數量。 + - 銷毀LP份額。 + - 將相應的代幣轉帳給用戶。 + - 更新儲備量。 + - 釋放Burn事件。 + +```solidity +// 移除流动性,销毁LP,转出代币 +// 转出数量 = (liquidity / totalSupply_LP) * reserve +// @param liquidity 移除的流动性数量 +function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) { + // 获取余额 + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + // 按LP的比例计算要转出的代币数量 + uint _totalSupply = totalSupply(); + amount0 = liquidity * balance0 / _totalSupply; + amount1 = liquidity * balance1 / _totalSupply; + // 检查代币数量 + require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED'); + // 销毁LP + _burn(msg.sender, liquidity); + // 转出代币 + token0.transfer(msg.sender, amount0); + token1.transfer(msg.sender, amount1); + // 更新储备量s + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Burn(msg.sender, amount0, amount1); +} +``` + +#### 交易 + +有了这一核心公式后,我们可以着手实现交易功能了。下面的 swap() 函数实现了交易代币的功能,主要步骤如下: + +- 用户在调用函数时指定用于交换的代币数量,交换的代币地址,以及换出另一种代币的最低数量。 +- 判断是 token0 交换 token1,还是 token1 交换 token0。 +- 利用上面的公式,计算交换出代币的数量。 +- 判断交换出的代币是否达到了用户指定的最低数量,这里类似于交易的滑点。 +- 将用户的代币转入合约。 +- 将交换的代币从合约转给用户。 +- 更新合约的代币储备量。 +- 释放 Swap 事件。 + +```solidity +// swap代币 +// @param amountIn 用于交换的代币数量 +// @param tokenIn 用于交换的代币合约地址 +// @param amountOutMin 交换出另一种代币的最低数量 +function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){ + require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT'); + require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN'); + + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + + if(tokenIn == token0){ + // 如果是token0交换token1 + tokenOut = token1; + // 计算能交换出的token1数量 + amountOut = getAmountOut(amountIn, balance0, balance1); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + // 进行交换 + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + }else{ + // 如果是token1交换token0 + tokenOut = token0; + // 计算能交换出的token1数量 + amountOut = getAmountOut(amountIn, balance1, balance0); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + // 进行交换 + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + } + + // 更新储备量 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut)); +} +``` + +### simpleSwap.sol +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract SimpleSwap is ERC20 { + // 代币合约 + IERC20 public token0; + IERC20 public token1; + + // 代币储备量 + uint public reserve0; + uint public reserve1; + + // 事件 + event Mint(address indexed sender, uint amount0, uint amount1); + event Burn(address indexed sender, uint amount0, uint amount1); + event Swap( + address indexed sender, + uint amountIn, + address tokenIn, + uint amountOut, + address tokenOut + ); + + // 构造器,初始化代币地址 + constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") { + token0 = _token0; + token1 = _token1; + } + + // 取两个数的最小值 + function min(uint x, uint y) internal pure returns (uint z) { + z = x < y ? x : y; + } + + // 计算平方根 babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) + function sqrt(uint y) internal pure returns (uint z) { + if (y > 3) { + z = y; + uint x = y / 2 + 1; + while (x < z) { + z = x; + x = (y / x + x) / 2; + } + } else if (y != 0) { + z = 1; + } + } + + // 添加流动性,转进代币,铸造LP + // 如果首次添加,铸造的LP数量 = sqrt(amount0 * amount1) + // 如果非首次,铸造的LP数量 = min(amount0/reserve0, amount1/reserve1)* totalSupply_LP + // @param amount0Desired 添加的token0数量 + // @param amount1Desired 添加的token1数量 + function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){ + // 将添加的流动性转入Swap合约,需事先给Swap合约授权 + token0.transferFrom(msg.sender, address(this), amount0Desired); + token1.transferFrom(msg.sender, address(this), amount1Desired); + // 计算添加的流动性 + uint _totalSupply = totalSupply(); + if (_totalSupply == 0) { + // 如果是第一次添加流动性,铸造 L = sqrt(x * y) 单位的LP(流动性提供者)代币 + liquidity = sqrt(amount0Desired * amount1Desired); + } else { + // 如果不是第一次添加流动性,按添加代币的数量比例铸造LP,取两个代币更小的那个比例 + liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1); + } + + // 检查铸造的LP数量 + require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED'); + + // 更新储备量 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + // 给流动性提供者铸造LP代币,代表他们提供的流动性 + _mint(msg.sender, liquidity); + + emit Mint(msg.sender, amount0Desired, amount1Desired); + } + + // 移除流动性,销毁LP,转出代币 + // 转出数量 = (liquidity / totalSupply_LP) * reserve + // @param liquidity 移除的流动性数量 + function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) { + // 获取余额 + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + // 按LP的比例计算要转出的代币数量 + uint _totalSupply = totalSupply(); + amount0 = liquidity * balance0 / _totalSupply; + amount1 = liquidity * balance1 / _totalSupply; + // 检查代币数量 + require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED'); + // 销毁LP + _burn(msg.sender, liquidity); + // 转出代币 + token0.transfer(msg.sender, amount0); + token1.transfer(msg.sender, amount1); + // 更新储备量 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Burn(msg.sender, amount0, amount1); + } + + // 给定一个资产的数量和代币对的储备,计算交换另一个代币的数量 + // 由于乘积恒定 + // 交换前: k = x * y + // 交换后: k = (x + delta_x) * (y + delta_y) + // 可得 delta_y = - delta_x * y / (x + delta_x) + // 正/负号代表转入/转出 + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) { + require(amountIn > 0, 'INSUFFICIENT_AMOUNT'); + require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY'); + amountOut = amountIn * reserveOut / (reserveIn + amountIn); + } + + // swap代币 + // @param amountIn 用于交换的代币数量 + // @param tokenIn 用于交换的代币合约地址 + // @param amountOutMin 交换出另一种代币的最低数量 + function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){ + require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT'); + require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN'); + + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + + if(tokenIn == token0){ + // 如果是token0交换token1 + tokenOut = token1; + // 计算能交换出的token1数量 + amountOut = getAmountOut(amountIn, balance0, balance1); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + // 进行交换 + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + }else{ + // 如果是token1交换token0 + tokenOut = token0; + // 计算能交换出的token1数量 + amountOut = getAmountOut(amountIn, balance1, balance0); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + // 进行交换 + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + } + + // 更新储备量 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut)); + } +} +``` +看來後續要在多學一點.. \ No newline at end of file