本文最後更新於: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