---
# System prepended metadata

title: Ryan 第五週作業
tags: [Solidity 工程師實戰營第 5 期]

---

# 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 期`