# Ryan 第五週作業 ## 基本題 ### 1. 部署自己的 AMM 合約 開源並提交合約地址 實作 swap, addLiquidity, removeLiquidity 提交截圖並說明各操作 #### 合約地址 : https://goerli.etherscan.io/address/0x988735d2e12af2c4b6a85d9d1a5b895b2e3fd092#code ![](https://i.imgur.com/PKY9GVB.png) #### 程式碼 ```solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.18; contract AMM { IERC20 public immutable token0; //流動池token0 IERC20 public immutable token1; //流動池token1 uint256 public reserve0; //token0數量 uint256 public reserve1; //token1數量 uint256 public totalSupply; //目前總數 mapping(address => uint256) public balanceOf; //用戶的流動性token map constructor(address _token0, address _token1) { token0 = IERC20(_token0); token1 = IERC20(_token1); } //用戶提供token0、token1時,增加該用戶流動性token及增加總數 function _mint(address _to, uint256 _amount) private { balanceOf[_to] += _amount; totalSupply += _amount; } //用戶撤回token0、token1時,減少該用戶流動性token function _burn(address _from, uint256 _amount) private { balanceOf[_from] -= _amount; totalSupply -= _amount; } //更新token0、token1數量 function _update(uint256 _reserve0, uint256 _reserve1) private { reserve0 = _reserve0; reserve1 = _reserve1; } //幣幣交換 function swap(address _tokenIn, uint256 _amountIn) external returns (uint256 amountOut) { //判斷是否為token0 or token1 require( _tokenIn == address(token0) || _tokenIn == address(token1), "invalid token" ); require(_amountIn > 0, "amount in = 0"); // 判斷 token0, token1 順序 bool isToken0 = _tokenIn == address(token0); (IERC20 tokenIn, IERC20 tokenOut, uint256 reserveIn, uint256 reserveOut) = isToken0 ? (token0, token1, reserve0, reserve1) : (token1, token0, reserve1, reserve0); // 把tokenIn轉給給合約 tokenIn.transferFrom(msg.sender, address(this), _amountIn); //計算 0.3% 手續費 uint256 amountInWithFee = (_amountIn * 997) / 1000; // 透過 x * y = k 計算該換多少 amountOut = (reserveOut * amountInWithFee) / (reserveIn + amountInWithFee); // 把tokenOut轉給該用戶 tokenOut.transfer(msg.sender, amountOut); // 更新token0、token1數量 _update(token0.balanceOf(address(this)), token1.balanceOf(address(this))); } //添加進資金池 function addLiquidity(uint256 _amount0, uint256 _amount1) external returns (uint256 shares) { token0.transferFrom(msg.sender, address(this), _amount0); token1.transferFrom(msg.sender, address(this), _amount1); // 需添加等值流動性 if (reserve0 > 0 || reserve1 > 0) { require(reserve0 * _amount1 == reserve1 * _amount0, "x / y != dx / dy"); } // 第一次添加 if (totalSupply == 0) { // 流動性 token 數量計算 = √ (amount0 * amount1) shares = _sqrt(_amount0 * _amount1); } else { // 計算流動性 token 比例,精度問題取小的 shares = _min( (_amount0 * totalSupply) / reserve0, (_amount1 * totalSupply) / reserve1 ); } require(shares > 0, "shares = 0"); _mint(msg.sender, shares); _update(token0.balanceOf(address(this)), token1.balanceOf(address(this))); } //從資金池撤回 function removeLiquidity( uint256 _shares ) external returns (uint256 amount0, uint256 amount1) { uint256 bal0 = token0.balanceOf(address(this)); uint256 bal1 = token1.balanceOf(address(this)); // 計算該取回的 token0 及 token1 數量 amount0 = (_shares * bal0) / totalSupply; amount1 = (_shares * bal1) / totalSupply; require(amount0 > 0 && amount1 > 0, "amount0 or amount1 = 0"); _burn(msg.sender, _shares); _update(bal0 - amount0, bal1 - amount1); token0.transfer(msg.sender, amount0); token1.transfer(msg.sender, amount1); } //開根號 function _sqrt(uint256 y) private pure returns (uint256 z) { if (y > 3) { z = y; uint256 x = y / 2 + 1; while (x < z) { z = x; x = (y / x + x) / 2; } } else if (y != 0) { z = 1; } } //取最小值 function _min(uint256 x, uint256 y) private pure returns (uint256) { return x <= y ? x : y; } } interface IERC20 { function totalSupply() external view returns (uint256); function balanceOf(address account) external view returns (uint256); function transfer(address recipient, uint256 amount) external returns (bool); function allowance(address owner, address spender) external view returns (uint256); function approve(address spender, uint256 amount) external returns (bool); function transferFrom( address sender, address recipient, uint256 amount ) external returns (bool); event Transfer(address indexed from, address indexed to, uint256 amount); event Approval(address indexed owner, address indexed spender, uint256 amount); } ``` #### token0為之前上課創建自己的ERC20代幣 RyanToken (RT) https://goerli.etherscan.io/token/0xD0FA61a4C4fa1057B9104b390fA2221b9EEBB561 #### token1為之前上課同組同學創建的ERC20代幣 Garrick (GHC) https://goerli.etherscan.io/token/0x66081df82eb1fcc32831b902fa733f92fd67770f?a=0x7a4d6c296b28460cda81fb584234a15fc105e182 #### 先查看目前資產 ![](https://i.imgur.com/fjdelex.png) #### 向token0 RyanToken (RT) 取得授權approve ![](https://i.imgur.com/tlcJpZQ.png) ![](https://i.imgur.com/c1G8IRX.png) #### 向token1 Garrick (GHC) 取得授權approve ![](https://i.imgur.com/lobH5qg.png) ![](https://i.imgur.com/oYQJY4Z.png) #### 批准成功 ![](https://i.imgur.com/KXuFwHr.png) #### addLiquidity 添加進資金池 第一次添加 token0 20個,token1 10個 ![](https://i.imgur.com/umemUl6.png) 成功添加進資金池 ![](https://i.imgur.com/1DFmLUe.png) 第二次添加 token0 20000個,token1 10000個 ![](https://i.imgur.com/p4xQnYX.png) #### 查看資金池目前狀態 ![](https://i.imgur.com/2guUHXD.png) #### swap 幣幣交換 使用token0 100個 換 token1 ![](https://i.imgur.com/8PYcGLD.png) ![](https://i.imgur.com/qdj7iIv.png) #### 查看資金池目前狀態 ![](https://i.imgur.com/azNkewz.png) #### removeLiquidity 從資金池撤回 先查看目前我擁有的流動性token數量 ![](https://i.imgur.com/D6BMXRj.png) 輸入數量並撤回 ![](https://i.imgur.com/CVYznH1.png) ![](https://i.imgur.com/MngK417.png) ### 2. 選擇兩個前 20 大的 DeFi 協議操作,並詳細說明該協議的功能及特色 提交交易的 Tx hash 以及協議地址寫下詳細的操作過程,例如:使用 Uniswap 將 0.1 ETH 換成 … DAI 說明該操作使用協議中智能合約的哪些 function 並說明其中程式碼共做了哪些鏈上狀態改變 #### 1. 使用uniswap交易所進行幣幣交換 Tx hash : [0x108e8d995c23969f27bd01aa640dcffa9a95cef64657e9205d906824bbdef298](https://goerli.etherscan.io/tx/0x108e8d995c23969f27bd01aa640dcffa9a95cef64657e9205d906824bbdef298) ![](https://i.imgur.com/z7vVj6L.png) ![](https://i.imgur.com/T20Hs9N.png) ![](https://i.imgur.com/geAH1dQ.png) 查看Deposit function ![](https://i.imgur.com/5QDmeoN.png) Deposit function改變了鏈上的該用戶的WETH餘額 ## 進階題 ### 1. 實作 Staking 功能的合約,質押 A token ,可獲得 B token 作為獎勵設定 B token 獎勵發放期間 設定 B token 發放數量 ```solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.18; contract StakingRewards { IERC20 public immutable stakingToken; //質押的token IERC20 public immutable rewardsToken; //獎勵的token address public owner; // 支付獎勵的持續時間(以秒為單位) uint public duration; // 獎勵結束時間戳 uint public finishAt; // 最後更新時間和獎勵完成時間的最小值 uint public updatedAt; // 每秒支付的獎勵 uint public rewardRate; // (獎勵率 * dt * 1e18 / 總供應量)的總和 uint public rewardPerTokenStored; // 用戶地址 => rewardPerTokenStored mapping(address => uint) public userRewardPerTokenPaid; // 用戶地址 => 待領取獎勵 mapping(address => uint) public rewards; // 總質押 uint public totalSupply; // 用戶地址 => 質押金額 mapping(address => uint) public balanceOf; constructor(address _stakingToken, address _rewardToken) { owner = msg.sender; stakingToken = IERC20(_stakingToken); rewardsToken = IERC20(_rewardToken); } modifier onlyOwner() { require(msg.sender == owner, "not authorized"); _; } modifier updateReward(address _account) { rewardPerTokenStored = rewardPerToken(); updatedAt = lastTimeRewardApplicable(); if (_account != address(0)) { rewards[_account] = earned(_account); userRewardPerTokenPaid[_account] = rewardPerTokenStored; } _; } function lastTimeRewardApplicable() public view returns (uint) { return _min(finishAt, block.timestamp); } function rewardPerToken() public view returns (uint) { if (totalSupply == 0) { return rewardPerTokenStored; } return rewardPerTokenStored + (rewardRate * (lastTimeRewardApplicable() - updatedAt) * 1e18) / totalSupply; } //質押 function stake(uint _amount) external updateReward(msg.sender) { require(_amount > 0, "amount = 0"); stakingToken.transferFrom(msg.sender, address(this), _amount); balanceOf[msg.sender] += _amount; totalSupply += _amount; } //領回 function withdraw(uint _amount) external updateReward(msg.sender) { require(_amount > 0, "amount = 0"); balanceOf[msg.sender] -= _amount; totalSupply -= _amount; stakingToken.transfer(msg.sender, _amount); } //計算質押獎勵 function earned(address _account) public view returns (uint) { return ((balanceOf[_account] * (rewardPerToken() - userRewardPerTokenPaid[_account])) / 1e18) + rewards[_account]; } //領取質押獎勵 function getReward() external updateReward(msg.sender) { uint reward = rewards[msg.sender]; if (reward > 0) { rewards[msg.sender] = 0; rewardsToken.transfer(msg.sender, reward); } } //設定獎勵結束時間戳 function setRewardsDuration(uint _duration) external onlyOwner { require(finishAt < block.timestamp, "reward duration not finished"); duration = _duration; } //通知獎勵金額 function notifyRewardAmount( uint _amount ) external onlyOwner updateReward(address(0)) { if (block.timestamp >= finishAt) { rewardRate = _amount / duration; } else { uint remainingRewards = (finishAt - block.timestamp) * rewardRate; rewardRate = (_amount + remainingRewards) / duration; } require(rewardRate > 0, "reward rate = 0"); require( rewardRate * duration <= rewardsToken.balanceOf(address(this)), "reward amount > balance" ); finishAt = block.timestamp + duration; updatedAt = block.timestamp; } //取最小數 function _min(uint x, uint y) private pure returns (uint) { return x <= y ? x : y; } } interface IERC20 { function totalSupply() external view returns (uint); function balanceOf(address account) external view returns (uint); function transfer(address recipient, uint amount) external returns (bool); function allowance(address owner, address spender) external view returns (uint); function approve(address spender, uint amount) external returns (bool); function transferFrom( address sender, address recipient, uint amount ) external returns (bool); event Transfer(address indexed from, address indexed to, uint value); event Approval(address indexed owner, address indexed spender, uint value); } ``` 建立質押Token ![](https://i.imgur.com/g1wEAti.png) 建立獎勵Token ![](https://i.imgur.com/epo4sTR.png) Mint 1000個質押Token給自己 ![](https://i.imgur.com/1n9kT6U.png) Mint 100000個獎勵Token給合約 ![](https://i.imgur.com/Qag4xEG.png) 設定發放時間為1天 = 86400秒 ![](https://i.imgur.com/kEtnTas.png) 設定獎勵Token發放數量 ![](https://i.imgur.com/cQyWy5r.png) approve給合約 ![](https://i.imgur.com/2rwP3Fl.png) 質押100個質押Token ![](https://i.imgur.com/eVnBjFH.png) 查看質押獎勵Token ![](https://i.imgur.com/Uir042M.png) 領取質押獎勵Token,查看帳戶裡面有多少個獎勵Token ![](https://i.imgur.com/PF42I3P.png) 領回質押Token ![](https://i.imgur.com/xUutZVC.png) ### 2. 研究 DeFi 合約審計報告,找一個 DeFi 協議並查看其審計報告後寫下心得 UniSwap Audit ![](https://i.imgur.com/fXugDC3.png) ![](https://i.imgur.com/A52onVU.png) ![](https://i.imgur.com/JTFpeAN.png) ###### tags: `Solidity 工程師實戰營第 5 期`