# 第五屆 KryptoCamp Solidity 實戰營
### 第二組專題-實作去中心化交易所
---
### Table of Content
#### 1. 誌謝
#### 2. 專題成員介紹
#### 3. 專題主題
#### 4. 專題內容功能列表及Demo網址
#### 5. 程式碼GitHub
#### 6. Token Address
#### 7. 功能說明
#### 8. 流程及功能Demo展示
#### 9. 參考資料
---
## 1. 誌謝
### Kryptocamp 主任 Casper
---
### 教師
### Hazel

---
### 懷恩

---
### Ray

---
### 助教 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. 功能說明

---
#### About US 關於我們
* 該專案簡易說明及組員介紹

---
#### Wallet 錢包

<font style="font-size:22px">左側顯示錢包地址、連接鏈的名稱、Import Token、資產(Token and balance)
右側顯示總交易紀錄(Block number / Date / Function / Gas fee / Address)</font>
---
* 整合圓餅圖及折線圖顯示每個api function每月的gas fee合計及波動(近期6個月)
圓餅圖

---
折線圖

---
#### Swap 幣幣交換

<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 流動性資金池

<font style="font-size:22px">於左側選擇資金池,於右側可以選擇添加或是取回</font>
---
<!--  -->
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 質押

<font style="font-size:22px">於左側Staking Tokens選擇質押Token,於右側Desposit可添加</font>
---

<font style="font-size:22px">Withdraw取回質押Token</font>
---

<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 空投

<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展示

---
#### 先領取空投幣,可用來交換其他代幣
* Get AirDrop Token (請領取1次以上)

---
* Get AirDrop Token Success

---
#### Swap 換其他代幣
* Approve 1個 AirDrop Token

---
* Approve Token Success

---
* Swap 1個 T1 Token

---
* Swap Token Success

---
* Approve 1個 AirDrop Token

---
* Approve Token Success

---
* Swap 1個 T2 Token

---
* Swap Token Success

---
#### Pool 添加流動性
* Approve T1 Token

---
* Approve Token Success

---
* Approve T2 Token

---
* Approve Token Success

---
* Deposit T1 Token & T2 Token

---
* Add Token Success

---
* Withdraw Token

---
* 確認支付Gas Fee

---
* Withdraw Token Success

---
#### Staking 質押
* Approve T1 Token

---
* Approve T1 Token Success

---
* Staking T1 Token

---
* Staking T1 Token Success

---
* Get Reward Token

---
* Get Reward Token Success

---
* Withdraw Token

---
* Withdraw Token Success

---
#### 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}]"}