# 第五屆 KryptoCamp Solidity 實戰營 ### 第二組專題-實作去中心化交易所 --- ### Table of Content #### 1. 誌謝 #### 2. 專題成員介紹 #### 3. 專題主題 #### 4. 專題內容功能列表及Demo網址 #### 5. 程式碼GitHub #### 6. Token Address #### 7. 功能說明 #### 8. 流程及功能Demo展示 #### 9. 參考資料 --- ## 1. 誌謝 ### Kryptocamp 主任 Casper --- ### 教師 ### Hazel ![](https://hackmd.io/_uploads/BkDgmd4Bh.png) --- ### 懷恩 ![](https://hackmd.io/_uploads/BksHE24Hn.png) --- ### Ray ![](https://hackmd.io/_uploads/rk9qfdVSn.png) --- ### 助教 Bonnie --- ## 2. 專題成員介紹 #### Bonnie、許家豪(Ryan)、錢昱名(企鵝)、鄭佳毓、陳信仲(Garrick) --- ## 3. 專題主題 #### 本專題主題為實作去中心化交易所 --- 去中心化交易所(DEX)相較於中心化交易所(CEX): #### (1) 資產管理權及交易透明度對於用戶來說較有優勢 | 比較項目 | CEX | DEX | | -------- | -------- | -------- | | 資產管理權 | 用戶交給CEX | 用戶自己 | | 交易量 | 高 | 低 | | 交易透明度 | 低 | 高(鏈上交易記錄) | --- #### (2) 但交易深度仍較低,因此專題開始時決定開發以下功能期望提高用戶參與度 * 先是採用空投方式讓用戶取得治理代幣 * 再以此治理代幣進行兌換或提供流動性 * 接著以其他幣種進行質押後再取得治理代幣作為獎勵 * 並提供查詢錢包交易記錄 * 交易Gas Fee統計圖 --- ### 4. 專題內容功能列表及Demo網址 <table border="1" width="80%" height="auto" cellspacing="5" cellpadding="5" style="font-size: 28px"> <tr> <th>功能編號</th> <th>功能名稱</th> <th>開發者</th> </tr> <tr> <td>1</td> <td>錢包連接、顯示</td> <td>陳信仲(Garrick)</td> </tr> <tr> <td>2</td> <td>Swap幣幣交換</td> <td>錢昱名(企鵝)</td> </tr> <tr> <td>3</td> <td>流動性資金池 添加/取出</td> <td>許家豪(Ryan)</td> </tr> <tr> <td>4</td> <td>質押ERC20功能</td> <td>陳信仲(Garrick)</td> </tr> <tr> <td>5</td> <td>ERC代幣及空投機制</td> <td>鄭佳毓</td> </tr> <tr> <td>6</td> <td>DApp開發</td> <td>許家豪(Ryan)</td> </tr> </table> --- #### Demo網頁網址 : https://kryptocampdex.vercel.app #### Demo影片介紹網址 : https://vimeo.com/824454806/5763a288a6 --- ## 5. 程式碼GitHub #### Contract : https://github.com/ryan19910912/dex-contract #### React : https://github.com/ryan19910912/dex-react --- ## 6. Token Address <font style="font-size:22px">合約佈署在Sepolia測試鏈,若需測試請先至Metamask將下方五種Token導入錢包中(Import Token) 或可以由Demo網頁Import Token,合約地址如下:</font> <table border="1" width="80%" height="auto" cellspacing="5" cellpadding="5" style="font-size: 22px"> <tr> <th>Token</th> <th>Address</th> </tr> <tr> <td>空投幣 AirDrop (AIRT)</td> <td>0x33B45bE67ca6eEBfD5d46c2Ca4d43ad8709073EE</td> </tr> <tr> <td>獎勵幣 RewardToken (RWT)</td> <td>0x2d335e7b6091918a5ef43A94eafE40DC7652fAe8</td> </tr> <tr> <td>自定義幣 Token1 (T1)</td> <td>0xf5295511a963e8FEbE269875a53cf4b5801e033A</td> </tr> <tr> <td>自定義幣 Token2 (T2)</td> <td>0x070648778d8095979F40A7942c1D27797d6662bC</td> </tr> <tr> <td>自定義幣 Token3 (T3)</td> <td>0xd8aC7143742C3EC1387512b0a673AA948215FC8F</td> </tr> </table> --- ## 7. 功能說明 ![](https://hackmd.io/_uploads/Hy3MDIXV3.png) --- #### About US 關於我們 * 該專案簡易說明及組員介紹 ![](https://hackmd.io/_uploads/rJrfrpfVn.png) --- #### Wallet 錢包 ![](https://hackmd.io/_uploads/ry3VXm-r3.png) <font style="font-size:22px">左側顯示錢包地址、連接鏈的名稱、Import Token、資產(Token and balance) 右側顯示總交易紀錄(Block number / Date / Function / Gas fee / Address)</font> --- * 整合圓餅圖及折線圖顯示每個api function每月的gas fee合計及波動(近期6個月) 圓餅圖 ![](https://hackmd.io/_uploads/SJO977ZHn.png) --- 折線圖 ![](https://hackmd.io/_uploads/H10oX7-rn.png) --- #### Swap 幣幣交換 ![](https://hackmd.io/_uploads/BJXcS6MEn.png) <font style="font-size:22px">左側Trending Pairs選取交易對,執行幣幣交換 空投幣可兌換其他幣種,但不可被兌換</font> --- Swap 幣幣交換 智能合約 (AMM) :::spoiler ```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 event removeLiquidityEvent(uint256 amount0, uint256 amount1); 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)) ); } //空投幣交換 1空投幣可換2個代幣 function airdropswap(address _tokenIn, uint256 _amountIn) external returns (uint256 amountOut) { //判斷是否為token0 require( _tokenIn == address(token0), "invalid token" ); require(_amountIn > 0, "amount in = 0"); // 把tokenIn轉給給合約 token0.transferFrom(msg.sender, address(this), _amountIn); amountOut = _amountIn * 2; // 把token1轉給該用戶 token1.transfer(msg.sender, amountOut); // 更新token0、token1數量 _update( token0.balanceOf(address(this)), token1.balanceOf(address(this)) ); } // 計算swap能兌換的數量供Dapp參考 function swapCalculate(address _tokenIn, uint256 _amountIn) external view 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); (uint256 reserveIn, uint256 reserveOut) = isToken0 ? (reserve0, reserve1) : (reserve1, reserve0); //計算 0.3% 手續費 uint256 amountInWithFee = (_amountIn * 997) / 1000; amountOut = (reserveOut * amountInWithFee) / (reserveIn + amountInWithFee); } //添加進資金池 function addLiquidity(uint256 _amount0, uint256 _amount1) external returns (uint256 shares) { 需添加等值流動性 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"); token0.transferFrom(msg.sender, address(this), _amount0); token1.transferFrom(msg.sender, address(this), _amount1); _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); emit removeLiquidityEvent(amount0, 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 ); } ``` ::: --- #### Pool 流動性資金池 ![](https://hackmd.io/_uploads/BkLwL6MEn.png) <font style="font-size:22px">於左側選擇資金池,於右側可以選擇添加或是取回</font> --- <!-- ![](https://hackmd.io/_uploads/r1B08TMN3.png) --> Pool 流動性資金池 智能合約 (AMM) :::spoiler ```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 event removeLiquidityEvent(uint256 amount0, uint256 amount1); 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)) ); } //空投幣交換 1空投幣可換2個代幣 function airdropswap(address _tokenIn, uint256 _amountIn) external returns (uint256 amountOut) { //判斷是否為token0 require( _tokenIn == address(token0), "invalid token" ); require(_amountIn > 0, "amount in = 0"); // 把tokenIn轉給給合約 token0.transferFrom(msg.sender, address(this), _amountIn); amountOut = _amountIn * 2; // 把token1轉給該用戶 token1.transfer(msg.sender, amountOut); // 更新token0、token1數量 _update( token0.balanceOf(address(this)), token1.balanceOf(address(this)) ); } function swapCalculate(address _tokenIn, uint256 _amountIn) external view 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); (uint256 reserveIn, uint256 reserveOut) = isToken0 ? (reserve0, reserve1) : (reserve1, reserve0); //計算 0.3% 手續費 uint256 amountInWithFee = (_amountIn * 997) / 1000; amountOut = (reserveOut * amountInWithFee) / (reserveIn + amountInWithFee); } //添加進資金池 function addLiquidity(uint256 _amount0, uint256 _amount1) external returns (uint256 shares) { 需添加等值流動性 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"); token0.transferFrom(msg.sender, address(this), _amount0); token1.transferFrom(msg.sender, address(this), _amount1); _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); emit removeLiquidityEvent(amount0, 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 ); } ``` ::: --- #### Staking 質押 ![](https://hackmd.io/_uploads/SkWev6fV2.png) <font style="font-size:22px">於左側Staking Tokens選擇質押Token,於右側Desposit可添加</font> --- ![](https://hackmd.io/_uploads/HJlbwaMVh.png) <font style="font-size:22px">Withdraw取回質押Token</font> --- ![](https://hackmd.io/_uploads/rJpbvTf43.png) <font style="font-size:22px">Reward領取獎勵幣</font> --- Staking 質押 智能合約 :::spoiler ```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); } ``` ::: --- #### AirDrop 空投 ![](https://hackmd.io/_uploads/SJojw6MV3.png) <font style="font-size:22px"> 上方為領取空投規則,每個人最大領取數量為 1000 個,每個人領取時間間隔為 10 秒</font> <br/> <font style="font-size:22px"> 下方為顯示你已領取的數量及最後領取的時間</font> --- AirDrop 空投 智能合約 :::spoiler ```solidity= // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; //空投合約 contract Airdropper { using SafeERC20 for IERC20; address private owner; // 擁有者 uint256 public airdropPerAmount = 1; // 每人每次可以領取的空投數量,未設置則每人每次只能領一次 uint256 public airdropTotalAmount = 1; // 每人可以領取的空投總數,未設置則每人可領取最大數為1 uint256 public airdropTerm; // 多久可以領取一次(單位s),未設置則每人只能領一次 uint256 public airdropStartTime; // 空投開始時間(時間戳),未設置不限制 uint256 public airdropDeadline; // 空投截止時間(時間戳),未設置不限制 address public tokenAddress; // 空投token地址 mapping(address => uint256) public airdropRecord; // 每個人空投領取總額 mapping(address => uint256) public airdropTimeRecord; // 最後一次領取空投時間 constructor() { owner = msg.sender; } // 批量發放空投,dests和values兩個數組長度若相等,則給不同地址發放對應數量token,如果values只有一個元素,則每個地址發放等量token function doAirdrop(address[] memory dests, uint256[] memory values) external virtual returns (uint256) { // 批量發放一般為官方操作,不受時間、領取額度等以上各種條件限制 require(msg.sender == owner, "Airdropper: forbidden"); require(tokenAddress != address(0), "Airdropper: address not zero"); uint256 i = 0; while (i < dests.length) { uint256 sendAmount = values.length == 1 ? values[0] : values[i]; // 判斷當前合約中剩餘token是否夠發放數量,如果不夠則結束發放並返回已發放的最後一個索引 if (ERC20(tokenAddress).balanceOf(address(this)) < sendAmount) { break; } // 接收地址不為0,發放數量不為0,則執行發放 if (dests[i] != address(0) && sendAmount > 0) { IERC20(tokenAddress).safeTransfer(dests[i], sendAmount); } i++; } return i; } // 個人領取空投 function getAirdrop() external virtual returns (bool) { // token地址不能為0地址 require(tokenAddress != address(0), "Airdropper: address not zero"); // 每人每次可以領取的空投數量要大於0 require(airdropPerAmount > 0, "Airdropper: no parameter set"); // 當前時間要大於空投開始時間 require(block.timestamp >= airdropStartTime, "Airdropper: not started"); if (airdropTotalAmount > 0) { // 如果設置了 每人可以領取的空投總數 這個參數,則驗證已領取數量要小於這個總數 require( airdropRecord[msg.sender] < airdropTotalAmount, "Airdropper: total amount limit" ); } if (airdropTerm > 0) { // 如果設置了領取周期參數,則驗證當前時間減去上次領取時間大於這個週期 require( block.timestamp - airdropTimeRecord[msg.sender] > airdropTerm, "Airdropper: term limit" ); } else { // 如果沒有設置週期參數,則驗證沒有領取過可以領取,只能領1次 require( airdropRecord[msg.sender] == 0, "Airdropper: you have already received" ); } if (airdropDeadline > 0) { // 如果設置了空投截止時間,則驗證當前時間小於截止時間 require(airdropDeadline > block.timestamp, "Airdropper: deadline"); } // 驗證當前合約token數量夠發放數量 require( ERC20(tokenAddress).balanceOf(address(this)) >= airdropPerAmount, "Airdropper: insufficient assets" ); // 執行發放 IERC20(tokenAddress).safeTransfer(msg.sender, airdropPerAmount); // 累計領取總數 airdropRecord[msg.sender] += airdropPerAmount; // 記錄最後領取時間 airdropTimeRecord[msg.sender] = block.timestamp; return true; } // 充入token function recharge(address _tokenAddress, uint256 _amount) external virtual returns (bool) { require(msg.sender == owner, "Airdropper: forbidden"); require(_tokenAddress != address(0), "Airdropper: forbidden"); // 驗證充入的token和配置的地址一致 require( _tokenAddress == tokenAddress, "Airdropper: Error token address" ); // 執行充入token IERC20(tokenAddress).safeTransferFrom( msg.sender, address(this), _amount ); return true; } // 提出剩餘token function withdraw() external virtual returns (bool) { require(msg.sender == owner, "Airdropper: forbidden"); require(tokenAddress != address(0), "Airdropper: address not zero"); // 將剩餘token全部轉給合約發布者 IERC20(tokenAddress).safeTransfer( owner, ERC20(tokenAddress).balanceOf(address(this)) ); tokenAddress = address(0); // 重置token地址 return true; } /** * 以下是配置各個參數的接口,只有合約發布者可以調用 */ function setPerAmount(uint256 _airdropPerAmount) external virtual returns (bool) { require(msg.sender == owner, "Airdropper: forbidden"); airdropPerAmount = _airdropPerAmount; return true; } // 設定空投總數 function setTotalAmount(uint256 _airdropTotalAmount) external virtual returns (bool) { require(msg.sender == owner, "Airdropper: forbidden"); airdropTotalAmount = _airdropTotalAmount; return true; } // 設定多久可以領取一次(單位s),未設置則每人只能領一次 function setTerm(uint256 _airdropTerm) external virtual returns (bool) { require(msg.sender == owner, "Airdropper: forbidden"); airdropTerm = _airdropTerm; return true; } // 設定空投開始時間 function setStartTime(uint256 _airdropStartTime) external virtual returns (bool) { require(msg.sender == owner, "Airdropper: forbidden"); airdropStartTime = _airdropStartTime; return true; } // 設定空投結束時間 function setDeadline(uint256 _airdropDeadline) external virtual returns (bool) { require(msg.sender == owner, "Airdropper: forbidden"); airdropDeadline = _airdropDeadline; return true; } // 設定空投Token Address function setTokenAddress(address _tokenAddress) external virtual returns (bool) { require(msg.sender == owner, "Airdropper: forbidden"); tokenAddress = _tokenAddress; return true; } } ``` ::: --- #### 8. 流程及功能Demo展示 ![](https://hackmd.io/_uploads/S1Dql5743.png) --- #### 先領取空投幣,可用來交換其他代幣 * Get AirDrop Token (請領取1次以上) ![](https://hackmd.io/_uploads/HylMZVXNh.png) --- * Get AirDrop Token Success ![](https://hackmd.io/_uploads/r15mZE7V3.png) --- #### Swap 換其他代幣 * Approve 1個 AirDrop Token ![](https://hackmd.io/_uploads/rJJ2-VXV2.png) --- * Approve Token Success ![](https://hackmd.io/_uploads/S1eI7j4N2.png) --- * Swap 1個 T1 Token ![](https://hackmd.io/_uploads/Bkc4zVm43.png) --- * Swap Token Success ![](https://hackmd.io/_uploads/Hk9UGVX43.png) --- * Approve 1個 AirDrop Token ![](https://hackmd.io/_uploads/HJfGmVQ43.png) --- * Approve Token Success ![](https://hackmd.io/_uploads/BkJwQsE4n.png) --- * Swap 1個 T2 Token ![](https://hackmd.io/_uploads/S1NIXNQVh.png) --- * Swap Token Success ![](https://hackmd.io/_uploads/rJz5XN7Nh.png) --- #### Pool 添加流動性 * Approve T1 Token ![](https://hackmd.io/_uploads/rkkOB4mN2.png) --- * Approve Token Success ![](https://hackmd.io/_uploads/BybiQsNEh.png) --- * Approve T2 Token ![](https://hackmd.io/_uploads/By9lUEmE2.png) --- * Approve Token Success ![](https://hackmd.io/_uploads/BJ2gVi4V3.png) --- * Deposit T1 Token & T2 Token ![](https://hackmd.io/_uploads/r1QKUNXE2.png) --- * Add Token Success ![](https://hackmd.io/_uploads/HkhHNj4E3.png) --- * Withdraw Token ![](https://hackmd.io/_uploads/rJiewNmE2.png) --- * 確認支付Gas Fee ![](https://hackmd.io/_uploads/r1eGP4QNn.png) --- * Withdraw Token Success ![](https://hackmd.io/_uploads/BkZEwEQVn.png) --- #### Staking 質押 * Approve T1 Token ![](https://hackmd.io/_uploads/B13dFEXV3.png) --- * Approve T1 Token Success ![](https://hackmd.io/_uploads/SyTqEoENn.png) --- * Staking T1 Token ![](https://hackmd.io/_uploads/HJ2o5EQN2.png) --- * Staking T1 Token Success ![](https://hackmd.io/_uploads/rkK0c4XE2.png) --- * Get Reward Token ![](https://hackmd.io/_uploads/SkhKyrXVn.png) --- * Get Reward Token Success ![](https://hackmd.io/_uploads/HJIo1rQEn.png) --- * Withdraw Token ![](https://hackmd.io/_uploads/SJD01BXV2.png) --- * Withdraw Token Success ![](https://hackmd.io/_uploads/B1iMxHQNh.png) --- #### 9. 參考資料 <p style="font-size:22px"> (1) 網站UI/UX參考 : https://app.mute.io/swap </p> <p style="font-size:22px"> (2) 前端及合約程式參考 : 每一週的課程及作業 </p> <p style="font-size:22px"> (3) 流動性挖礦 : https://abmedia.io/what-is-yield-farming </p> <p style="font-size:22px"> (4) 空投機制 : https://blog.csdn.net/Meta_World/article/details/124392974 </p> <p style="font-size:22px"> (5) 圖像製作 : https://www.figma.com/ </p> <p style="font-size:22px"> (6) Etherscan Api : https://docs.etherscan.io/v/sepolia-etherscan/ </p> <p style="font-size:22px"> (7) ethers V5 : https://docs.ethers.org/v5/ </p> <p style="font-size:22px"> (8) thirweb : https://thirdweb.com/ </p> <p style="font-size:22px"> (9) chakra-ui : https://chakra-ui.com/ </p> <p style="font-size:22px"> (10) openzeppelin : https://www.openzeppelin.com/ </p> <p style="font-size:22px"> (11) react-icons : https://react-icons.github.io/react-icons/ </p> <p style="font-size:22px"> (12) React : https://react.dev/ </p> <p style="font-size:22px"> (13) Vite : https://vitejs.dev/ </p> ###### tags: `Solidity 工程師實戰營第 5 期`
{"metaMigratedAt":"2023-06-18T03:26:09.207Z","metaMigratedFrom":"YAML","title":"第五屆 KryptoCamp Solidity 實戰營","breaks":true,"contributors":"[{\"id\":\"23f0d768-b610-4bfb-b1ef-39559d3c0b50\",\"add\":36921,\"del\":31004},{\"id\":\"5a41ab49-aa4f-431d-9af6-7cab503c0b10\",\"add\":26816,\"del\":937},{\"id\":\"0a5ae2c2-4197-47e5-ace1-b363f05b90e7\",\"add\":1312,\"del\":924}]"}
    681 views