全端Defi平台

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

  • 目的: 篩選可接受的代幣種類
    • loop through 可接受的代幣list
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

  1. 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
  2. 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 真的要開始分配了…

  1. 設定 constructor + 宣告狀態變數,才能在合約中公開使用我們的 dappToken 合約地址
    1. 能夠使用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_tokenfau_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} #把大部分的治理幣轉去合約儲存,剩餘留給自己(KEPT_BALANCE)
)
tx.wait(1)
# dapp_token, weth_token, fau_token/dai
weth_token = get_contract("weth_token")#在本地上部屬假的weth token 回傳得到該合約的地址
fau_token = get_contract("fau_token")#在本地上部屬假的fau token
dict_of_allowed_tokens = { #每個合約會map到轉換地址
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
#in helpful_script.py 用來製作token名稱和地址之間的mapping
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