本文最後更新於:2022年2月8日 晚上
                  
                
              
            
            
              
                
                全端DeFi平台 目的: 建立一個全端的defi平台
1. 建立代幣合約 DappToken.sol 
1 2 3 4 5 6 7 pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract DappToken is ERC20 {     constructor() public ERC20("Dapp Token", "DAPP"){         _mint(msg.sender, 1000000000000000000000000);     } }
 
2. 建立平台交易合約TokenFarm.sol 1. 先決定交易規則 
stakeTokens()1 2 3 4 5 6 7 8 9 10 11 12 function stakeTokens(uint256 _amount, address _token) public {         require(_amount > 0, "Amount must be more than 0");         require(tokenIsAllowed(_token), "Token is currently no allowed");//來自下方兩個函數!         IERC20(_token).transferFrom(msg.sender, address(this), _amount);//把用戶的代幣轉移到平台合約(我們自己的合約)中         updateUniqueTokensStaked(msg.sender, _token);         stakingBalance[_token][msg.sender] = //更新合約中的用戶餘額             stakingBalance[_token][msg.sender] +             _amount;         if (uniqueTokensStaked[msg.sender] == 1) {             stakers.push(msg.sender);         }     }
 
tokenIsAllowed(address _token) 
1 2 3 4 5 6 7 8 9 10 11 12 13 address[] public allowedTokens; function tokenIsAllowed(address _token) public returns (bool) {         for (             uint256 allowedTokensIndex = 0;             allowedTokensIndex < allowedTokens.length;             allowedTokensIndex++         ) {             if (allowedTokens[allowedTokensIndex] == _token) {                 return true;             }         }         return false;     }
 
addAllowedTokens()
目的: 把可以交易的代幣推到允許名單「allowedTokens」上
因為只有合約主人可以使用, 函式要加onlyOwner + contract要加 Ownable 
 
 
 
1 2 3 4 5 contract TokenFarm is Ownable {  ..... function addAllowedTokens(address _token) public onlyOwner {         allowedTokens.push(_token);     } }
 
2. 轉移代幣 transferFrom 
和 transfer 的不同? 
目的: 接受用戶的動作請求
從msg.sender(呼叫此合約的人)到address(this)這個合約 
 
 
 
1 2 3 4 5 6 7 8 9 import "@openzeppelin/contracts/token/ERC20/IERC20.sol";//匯入ERC20的接口 //IERC20(_token)會回傳 abi function stakeTokens(uint256 _amount, address _token) public { ... //從msg.sender(呼叫此合約的人)到address(this)這個合約 IERC20(_token).transferFrom(msg.sender, address(this), _amount); ... }
 
補充: 我們可以使用 ERC20合約的兩個方法(method)來傳送代幣(token) contract development - Send tokens using approve and transferFrom vs only transfer - Ethereum Stack Exchange 
approve() and transferFrom() 他人想要動用自己錢包內的代幣
The approve + transferFrom is for a ==3 party transfer,== usually, but not necessarily that of an exchange, where the ==sender wishes to authorize a second party to transfer some tokens on their behalf.==
Sender ➜ approve(exchange, amount) 
Buyer ➜ executes trade on the Exchange 
Exchange ➜ transferFrom(sender, buyer, amount) 
user cannot call “transferFrom” directly because otherwise he/she can use anyone else’s token without the corresponding private key or wallet, so he need get approved by the original token owner to use a certain amount of their token  
 
 
 
 
transfer() 自己想要交易’
The transfer method is for a ==2 party transfer== , where a sender wishes to transfer some tokens to a receiver. 
Sender ➜ transfer(receiver, amount) 
user can unlock his wallet and call “transfer” to transfer his/her own token 
 
 
 
 
 
mapping stakingBalance 1 2 3 4 contract TokenFarm is Ownable { ... // mapping token address -> staker address -> amount     mapping(address => mapping(address => uint256)) public stakingBalance;     }
 
3. 分配治理幣issueToken 1 2 3 4 5 6 7 8 9 10 11 12 address[] public stakers; //紀錄質押名單 function stakeTokens(uint256 _amount, address _token) public {         ...         updateUniqueTokensStaked(msg.sender, _token);         stakingBalance[_token][msg.sender] =             stakingBalance[_token][msg.sender] +             _amount;         if (uniqueTokensStaked[msg.sender] == 1) {//如果只有一種代幣,才會該用戶加入質押名單  `stakers`             stakers.push(msg.sender);         }     }
 
查看用戶質押多少種不同代幣updateUniqueTokensStaked() mapping uniqueTokensStaked 
如果只有一種,就把該用戶加入質押名單  stakers
 
internal : 只有這個合約可以調用該函數 
 
1 2 3 4 5 6 7 8 mapping(address => uint256) public uniqueTokensStaked; function updateUniqueTokensStaked(address _user, address _token) internal { // mapping(address => mapping(address => uint256)) public stakingBalance;     if (stakingBalance[_token][_user] <= 0) {//如果這個user之前不存在        uniqueTokensStaked[_user] = uniqueTokensStaked[_user] + 1;//就幫她更新名單,回頭在 stakeTokens()被推入 stakers(質押名單)中         }     }
 
3.1 真的要開始分配了… 
設定 constructor +  宣告狀態變數,才能在合約中公開使用我們的 dappToken 合約地址
能夠使用dappToken 合約的函數! 
 
 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 IERC20 public dappToken; constructor(address _dappTokenAddress) public {         dappToken = IERC20(_dappTokenAddress);     }      function issueTokens() public onlyOwner {         // Issue tokens to all stakers         for (             uint256 stakersIndex = 0;             stakersIndex < stakers.length;             stakersIndex++         ) {         // 「基於目前質押的"全部"代幣數量」+「送出獎勵代幣」             address recipient = stakers[stakersIndex];             uint256 userTotalValue = getUserTotalValue(recipient);             dappToken.transfer(recipient, userTotalValue);         }     }
 
getUserTotalValue() ~~!!gas expensive!! 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function getUserTotalValue(address _user) public view returns (uint256) {         uint256 totalValue = 0;         require(uniqueTokensStaked[_user] > 0, "No tokens staked!"); //確保用戶在名單上         for (             uint256 allowedTokensIndex = 0;             allowedTokensIndex < allowedTokens.length;             allowedTokensIndex++         ) {             totalValue =                 totalValue +                 getUserSingleTokenValue(                     _user,                     allowedTokens[allowedTokensIndex]                 );         }         return totalValue;     }
 
✔COMMENT:  目前市場上有些protocal不使用getUserTotalValue()這種會花費昂貴的gas-fee的方式來分配代幣。他們直接用空投airdrop的方式讓用戶端送出請求!
getUserSingleTokenValue() 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function getUserSingleTokenValue(address _user, address _token)         public         view         returns (uint256)     {         if (uniqueTokensStaked[_user] <= 0) {             return 0;         }         // price of the token * stakingBalance[_token][user]         (uint256 price, uint256 decimals) = getTokenValue(_token);         return // 10000000000000000000 ETH         // ETH/USD -> 10000000000         // 10 * 100 = 1,000         ((stakingBalance[_token][_user] * price) / (10**decimals));     }
 
getTokenValue() setPriceFeedContract() mapping tokenPriceFeedMapping
目的: 透過和chainlink連結獲取pricefeed的資訊,得到匯率 
 
1 2 3 4 5 6 dependencies:    -  smartcontractkit/chainlink-brownie-contracts@0.2.1 compiler:    solc:      remappings:        -  "@chainlink=smartcontractkit/chainlink-brownie-contracts@0.2.1" 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; mapping(address => address) public tokenPriceFeedMapping;//map the token to the associated pricefeed function setPriceFeedContract(address _token, address _priceFeed)         public         onlyOwner //只能讓合約主人設定     {         tokenPriceFeedMapping[_token] = _priceFeed; //map the token to the pricewfeed     } function getTokenValue(address _token)         public         view         returns (uint256, uint256)     {         // priceFeedAddress         address priceFeedAddress = tokenPriceFeedMapping[_token];         AggregatorV3Interface priceFeed = AggregatorV3Interface(             priceFeedAddress         );         (, int256 price, , , ) = priceFeed.latestRoundData();         uint256 decimals = uint256(priceFeed.decimals());         return (uint256(price), decimals);     }
 
3. 取消質押 unstakeTokens 1 2 3 4 5 6 7 function unstakeTokens(address _token) public {         uint256 balance = stakingBalance[_token][msg.sender]; //獲取用戶目前餘額         require(balance > 0, "Staking balance cannot be 0");         IERC20(_token).transfer(msg.sender, balance);//用戶要返回治理幣!         stakingBalance[_token][msg.sender] = 0;         uniqueTokensStaked[msg.sender] = uniqueTokensStaked[msg.sender] - 1; // 去除該用戶質押的代幣種類     }
 
reentrancy attacks 4. 開始建立部屬 deploy.py 影片示範 
weth_token 和fau_token
因為都不是實際存在的測試網 所以需要部屬mock合約 
 
 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 KEPT_BALANCE = Web3.toWei(100 , "ether" )def  deploy_token_farm_and_dapp_token (front_end_update=False  ):     account = get_account()     dapp_token = DappToken.deploy({"from" : account})     token_farm = TokenFarm.deploy(          dapp_token.address,         {"from" : account},         publish_source=config["networks" ][network.show_active()].get("verify" ,False ),     )     tx = dapp_token.transfer(         token_farm.address, dapp_token.totalSupply() - KEPT_BALANCE, {"from" : account}      )     tx.wait(1 )          weth_token = get_contract("weth_token" )     fau_token = get_contract("fau_token" )     dict_of_allowed_tokens = {          dapp_token: get_contract("dai_usd_price_feed" ),         fau_token: get_contract("dai_usd_price_feed" ),         weth_token: get_contract("eth_usd_price_feed" ),     }     add_allowed_tokens(token_farm, dict_of_allowed_tokens, account)     if  front_end_update:         update_front_end()     return  token_farm, dapp_token
 
add_allowed_tokens()目的: 讓用戶可以新增代幣種類,並抓取合約
contract_to_mock
在helpful_script.py 
用來製作token名稱和地址之間的mapping 
MockDAI和MockWETH都是「本地的偽ERC代幣合約」 
 
 
 
1 2 3 4 5 6 7  contract_to_mock = {     "eth_usd_price_feed" : MockV3Aggregator,     "dai_usd_price_feed" : MockV3Aggregator,     "fau_token" : MockDAI,     "weth_token" : MockWETH, }
 
1 2 3 4 5 6 7 8 def  add_allowed_tokens (token_farm, dict_of_allowed_tokens, account ):     for  token in  dict_of_allowed_tokens:         add_tx = token_farm.addAllowedTokens(token.address, {"from" : account})         add_tx.wait(1 )         set_tx = token_farm.setPriceFeedContract(             token.address, dict_of_allowed_tokens[token], {"from" : account})         set_tx.wait(1 )     return  token_farm
 
deploy_mocks()1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def  deploy_mocks (decimals=DECIMALS, initial_value=INITIAL_PRICE_FEED_VALUE ):     """     Use this script if you want to deploy mocks to a testnet     """      print (f"The active network is {network.show_active()} " )     print ("Deploying Mocks..." )     account = get_account()     print ("Deploying Mock Link Token..." )     link_token = LinkToken.deploy({"from" : account})     print ("Deploying Mock Price Feed..." )     mock_price_feed = MockV3Aggregator.deploy(         decimals, initial_value, {"from" : account}     )     print (f"Deployed to {mock_price_feed.address} " )     print ("Deploying Mock DAI..." )     dai_token = MockDAI.deploy({"from" : account})      print (f"Deployed to {dai_token.address} " )     print ("Deploying Mock WETH" )     weth_token = MockWETH.deploy({"from" : account})     print (f"Deployed to {weth_token.address} " )
 
主要參考來源: Solidity, Blockchain, and Smart Contract Course – Beginner to Expert Python Tutorial - YouTube