defi 质押 dapp 智能合约系统开发代码逻辑
Scaffold-eth 的核心是为以太坊上的快速原型开发提供了一个现成的堆栈,使开发者能够获得最先进的工具来快速学习/交付基于以太坊的 dApp。使用 Scaffold-eth 和 Alchemy,你可以轻松地在区块链上合成和部署代码。在本教程中,我们将使用 SpeedRunEthereum[2] Challenge #1[3]的基础代码,并且共同构建一个简单的质押 dApp。如果你不熟悉加密货币质押,它最好被概括为将持有的加密货币锁定/存入 DeFi 协议或智能合约以获得利息的过程。质押加密货币已成为许多 DeFi 协议的基石,并允许开发人员创建复杂的金融衍生产品。虽然大多数 DeFi 质押合同都非常复杂,系统 I34 开 I633 发 53I9 搭建,我们将通过一个最基本的合同来学习关键概念。
我们将一起学习以下有关质押的构件。
用 Scaffold-Eth 构建• 一起破解前端• 打造 Solidity “后端”
将 ETH 从钱包转移到智能合约,反之亦然
利用 Solidity 修改器让我们从了解 Scaffold-Eth 的工作原理开始吧!
下载 Scaffold-Eth,我们将使用 Scaffold-Eth 开发者环境来制作我们的智能合约,并将我们的前端 UI 放在一起。在开始之前,我想传达几个重要的细节,开发对接唯 hkkf5566,让大家牢记在心!Scaffold-Eth 在抽象化环境设置和前端依赖方面非常棒,这使它成为一个强大的工具。虽然有很多功能是由 Scaffold-Eth 自动处理的,但当你对整个开发者环境有了更扎实的掌握后,深入到代码中去了解其中一些功能是如何产生的,这一点很重要。让我们从 Fork SpeedRunEthereum[4]的 Challenge #1[5] 的基础代码库开始。
git clone https://github.com/scaffold-eth/scaffold-eth-challenges.git challenge-1-decentralized-stakingcd challenge-1-decentralized-stakinggit checkout challenge-1-decentralized-staking
yarn install 如果你已经成功跟上,你将能够在你的基础文件目录中出现一个名为 challenge-1-decentralized-staking 的新文件夹。在运行上述命令后,我们留下了一个充满文件的大文件夹。即使在继续学习代码之前,我们也应该熟悉 Scaffold-Eth 中关键文件的存储位置,这样我们就知道应该把重点放在哪里。在本教程中,我们将主要在 Staker.sol 和 App.jsx 上工作。
设置你的环境接下来,你需要为以下三个命令建立三个独立的终端。启动你的 React 前端。yarn start 启动你的 Hardhat 后端。yarn chain 编译、部署和发布你的 package/contracts 文件中的所有合同。yarn deploy 每当你更新你的合同时,运行 yarn deploy --reset 来 "刷新 "Scaffold-Eth 中的合同。很好! 现在你应该可以在 http://localhost:3000/,看到这个仓库的 UI 前台了。
熟悉 Scaffold-Eth,有一些细节需要处理在我们的默认视图中,我们有两个标签--Staker UI & Debug Contracts。
在你的前端页面上来回切换,看看不同的功能。系统 I34 开 I633 发 53I9 搭建,Staker UI 包含了所有我们将主要与之互动的前端组件。如果你点击所提供的按钮,你会发现大多数按钮还没有完全连接起来,你会立即遇到错误。看一下 Staker UI。你会注意到它是如此的简陋的! 这就是我们主要要改变的东西。由于以太坊上的任何链上互动都需要 testnet ETH,所以你需要本地 testnet ETH 来开始砍价。首先,抓住你的本地主机钱包地址。点击右上角的 "复制 "按钮。
接下来,前往左下角。你将能够在这里访问本地龙头。要么在打开的字段中复制/粘贴你的地址,要么点击 "钱包 "图标• 在扩展视图中粘贴您的地址• 给自己发送一些测试 ETH
在为你的本地钱包充值后,你就可以与你的合约进行互动了第二个标签,Debug Contracts,是另一个前端显示,它包含了 Scaffold-Eth 的一个超级大功能!
在这个标签中,你可以看到你的合同。一旦你 yarn deploy 你的合约,并配置它正确地读取合约数据,它将自动生成一个裸露的 UI,允许你与你的合约功能互动。例如,在下面的例子中,我们可以通过我们的智能合约读取和写入信息,只需放入参数并点击 "发送"。有了 Scaffold-Eth,我们不需要只使用 CLI 命令,而是有一个更直观的原型设计方式。如果你想在 Debug Contracts 标签页中存储和查看一个变量,请确保将该变量设置为 public!棒极了!现在我们已经熟悉了 Scaffold-Eth,我们可以更深入地研究代码了。
潜入 Solidity 看一下我们的 Staker.sol 文件,我们发现我们有一个相当空的 Solidity 文件,里面有一堆注释,说明需要填写的内容。由于教程偏离了 Scaffold-Eth 的 Challenge #1,我们可以忽略这些注释,从以下代码开始。pragma solidity >=0.6.0 <0.7.0;
import "hardhat/console.sol";import "./ExampleExternalContract.sol";
contract Staker {ExampleExternalContract public exampleExternalContract;
constructor(address exampleExternalContractAddress) public {exampleExternalContract = ExampleExternalContract(exampleExternalContractAddress);}
}项目参数
在写出我们的智能合约代码之前,让我们来看看我们期望我们的质.押.DApp 如何工作
为了简单起见,我们只希望有一个用户与我们的质.押.DApp 互动
我们需要能够从 Staker 合约中存款和取款。质押是一个一次性的行为,这意味着一旦我们质.押.,就不能再重新质押。从合约中取款会移除整个本金余额和任何应计利息
Staker 合约的利息支付率为每秒钟 0.1 个 ETH,存入的 ETH 有资格获得利息累积。
合同部署后,Staker 合同应该从 Hh2 时间戳计数器开始。第一个期限应设置为 2 分钟,第二个期限设置为 4 分钟• 2 分钟的期限决定了钉子户能够存入资金的时期。(在 t=0 分钟和 t=2 分钟之间,质.押.用户可以存款)• 从存入资金到 2 分钟期限之间发生的所有区块都是有效的应计利息。• 在 2 分钟的提款期限过后,质.押.用户可以提取全部本金余额和任何应计利息,直到 4 分钟的期限到来。• 在额外的 2 分钟提款窗口过后,用户被阻止提取他们的资金,因为他们已经超时了。
如果质押用户还有资金,我们可以调用最后一个函数,将资金 "锁定 "在已经预装在 Scaffold-Eth 环境中的外部合同中,即 ExampleExternalContract.sol。虽然上面列出的质.押.参数可能看起来有点复杂,但许多现实生活中的质.押.dApps 都有类似的基元,用户有一个有限的存款和提款期。而且,许多 DApps 将抑制 "非生产性 "资本,这些资本在质.押.期结束后只是闲置在那里。有时,DeFi 协议甚至可能在等待期结束后吸收未付的存款,这与我们在教程中所说的最后一个参数类似。
Solidity Mappingss 在我们的智能合约中,我们将需要两个映射来帮助我们存储一些数据。特别是,我们需要一些东西来跟踪。
有多少 ETH 被存入合约中
存款发生的时间我们可以通过以下方式实现这一目标。mapping(address => uint256) public balances;mapping(address => uint256) public depositTimestamps;公共变量根据上面列出的准则[6],我们还需要一些不同的变量。uint256 public constant rewardRatePerSecond = 0.1 ether;uint256 public withdrawalDeadline = block.timestamp + 120 seconds;uint256 public claimDeadline = block.timestamp + 240 seconds;uint256 public currentBlock = 0;奖励率设定了质.押.本金的 ETH 的发放利率。提款和索赔的最后期限帮助我们设定质.押.机制开始/结束的最后期限。最后,我们有一个变量,用来保存当前区块。我们使用 block.timestamp + XXX seconds 来创建最后期限,正好是我们的合约启动后的 XXX 秒。作为一种计时机制,这肯定有点 "天真";你能想出更好的方法来实现这一点,例如,它更具有通用性?事件尽管我们不会将事件推送到我们的前端,但我们仍然应该确保我们在合同的关键部分发出事件,以确保我们保持最佳的编程实践。event Stake(address indexed sender, uint256 amount);event Received(address, uint);event Execute(address indexed sender, uint256 amount);现在我们已经锁定了关键参数/变量,我们可以继续制作我们将在教程中使用的具体函数。只读的时间函数正如项目参数中所述,许多不同的质.押.DApp 的功能都受制于 "时间锁",在特定的时间点启用/禁止某些行动。在这里,我们有两个不同的功能来管理提款窗口的开始和结束。 function withdrawalTimeLeft() public view returns (uint256 withdrawalTimeLeft) {if( block.timestamp >= withdrawalDeadline) {return (0);} else {return (withdrawalDeadline - block.timestamp);}}
function claimPeriodLeft() public view returns (uint256 claimPeriodLeft) {if( block.timestamp >= claimDeadline) {return (0);} else {return (claimDeadline - block.timestamp);}}这两个函数在设计上其实都非常熟悉。它们都有一个标准的 if -> else 语句。条件只是检查当前时间是否大于或小于公共变量部分[7]规定的最后期限。如果当前时间大于预先安排的最后期限,我们就知道最后期限已过,并返回 0 以表示 "状态变化 "已经发生。否则,我们只是返回在最后期限到来之前的剩余时间。修改器对于一个更深入的修改器的例子,请看 Solidity By Example。简而言之,Solidity 修改器是可以在函数调用之前和/或之后运行的代码片段。虽然它们有许多不同的用途,但最常见和最基本的用例之一是在特定条件未完全满足的情况下限制对某些功能的访问。在本教程中,我们将精确地使用修改器来帮助对关键功能进行把关,这些功能决定了我们的入股、提款和返还功能。
下面是我们使用的三个修改器。
modifier withdrawalDeadlineReached( bool requireReached ) {uint256 timeRemaining = withdrawalTimeLeft();if( requireReached ) {require(timeRemaining == 0, "Withdrawal period is not reached yet");} else {require(timeRemaining > 0, "Withdrawal period has been reached");}_;}
modifier claimDeadlineReached( bool requireReached ) {uint256 timeRemaining = claimPeriodLeft();if( requireReached ) {require(timeRemaining == 0, "Claim deadline is not reached yet");} else {require(timeRemaining > 0, "Claim deadline has been reached");}_;}
modifier notCompleted() {bool completed = exampleExternalContract.completed();require(!completed, "Stake already completed!");_;}修改器 withdrawalDeadlineReached(bool requireReached) &claimDeadlineReached(bool requireReached)都接受一个布尔参数,并检查以确保其各自的最后期限为真或假。修改器 notCompleted()的操作方式类似,但实际上它的性质更复杂一点,尽管它包含的代码行数更少。它实际上是从 Staker 外部的合同中调用一个函数 completed(),并检查它的返回值是真还是假,以确认该标志是否被切换。现在,让我们在接下来的几个函数上实现我们刚刚创建的修改器,用闸门限制访问。存款/质.押.功能在我们的入金函数中,我们使用先前创建的修改器,将 withdrawingDeadlineReached()中的参数设置为 false,将 claimDeadlineReached()设置为 false,因为我们不希望这两个期限已经过去。// Stake function for a user to stake ETH in our contract
function stake() public payable withdrawalDeadlineReached(false) claimDeadlineReached(false) {balances[msg.sender] = balances[msg.sender] + msg.value;depositTimestamps[msg.sender] = block.timestamp;emit Stake(msg.sender, msg.value);}该函数的其余部分在一个典型的 "存款 "场景中是相当标准的,我们的余额映射被更新以包括送入的资金。我们还用存款的当前时间来设置我们的存款时间戳,这样我们就可以在以后的利息计算中访问这个存储值。提款功能在我们的取款函数中,我们再次使用先前创建的修改器,但这次我们希望 drawalDeadlineReached()为真, claimDeadlineReached()为假。这组修改器/参数意味着我们处于提款窗口的最佳位置,因为提款时间到了,不会有任何处罚,而且我们还能得到利息。 /*Withdraw function for a user to remove their staked ETH inclusiveof both the principle balance and any accrued interest*/
function withdraw() public withdrawalDeadlineReached(true) claimDeadlineReached(false) notCompleted{require(balances[msg.sender] > 0, "You have no balance to withdraw!");uint256 individualBalance = balances[msg.sender];uint256 indBalanceRewards = individualBalance + ((block.timestamp-depositTimestamps[msg.sender])*rewardRatePerBlock);balances[msg.sender] = 0;
}该函数的其余部分做了几个重要步骤。1. 它检查以确保试图提取 ETH 的人实际上有一个非零的股份。2. 它通过计算从存款到取款的区块数,并乘以我们的利息常数,来计算欠下的 ETH 的利息金额。3. 它将用户的余额质.押.ETH 设为 0,这样就不会发生重复计算。4. 它将 ETH 从智能合约转移回用户的钱包。执行返还功能在这里,我们希望 claimDeadlineReached() 为真,因为非生产性资金的返还只能在 4 分钟后发生。同样地,我们希望 notCompleted 为真,因为这个 DApp 只设计为单一用途。 /*Allows any user to repatriate "unproductive" funds that are left in the staking contractpast the defined withdrawal period*/
function execute() public claimDeadlineReached(true) notCompleted {uint256 contractBalance = address(this).balance;exampleExternalContract.complete{value: address(this).balance}();}其余的功能。1. 抓取 Staker 合约中的 ETH 的当前余额 2. 将 ETH 发送到 repo 的 exampleExternalContract 中。如果你到目前为止一直跟着 Solidity 走,你的 Staker.sol 应该是下面这个样子。// SPDX-License-Identifier: MITpragma solidity 0.8.4;
import "hardhat/console.sol";import "./ExampleExternalContract.sol";
contract Staker {
ExampleExternalContract public exampleExternalContract;
mapping(address => uint256) public balances;mapping(address => uint256) public depositTimestamps;
uint256 public constant rewardRatePerSecond = 0.1 ether;uint256 public withdrawalDeadline = block.timestamp + 120 seconds;uint256 public claimDeadline = block.timestamp + 240 seconds;uint256 public currentBlock = 0;
// Eventsevent Stake(address indexed sender, uint256 amount);event Received(address, uint);event Execute(address indexed sender, uint256 amount);
// Modifiers/*Checks if the withdrawal period has been reached or not*/modifier withdrawalDeadlineReached( bool requireReached ) {uint256 timeRemaining = withdrawalTimeLeft();if( requireReached ) {require(timeRemaining == 0, "Withdrawal period is not reached yet");} else {require(timeRemaining > 0, "Withdrawal period has been reached");}_;}
/*Checks if the claim period has ended or not*/modifier claimDeadlineReached( bool requireReached ) {uint256 timeRemaining = claimPeriodLeft();if( requireReached ) {require(timeRemaining == 0, "Claim deadline is not reached yet");} else {require(timeRemaining > 0, "Claim deadline has been reached");}_;}
/*Requires that the contract only be completed once!*/modifier notCompleted() {bool completed = exampleExternalContract.completed();require(!completed, "Stake already completed!");_;}
constructor(address exampleExternalContractAddress){exampleExternalContract = ExampleExternalContract(exampleExternalContractAddress);}
// Stake function for a user to stake ETH in our contractfunction stake() public payable withdrawalDeadlineReached(false) claimDeadlineReached(false){balances[msg.sender] = balances[msg.sender] + msg.value;depositTimestamps[msg.sender] = block.timestamp;emit Stake(msg.sender, msg.value);}
/*Withdraw function for a user to remove their staked ETH inclusiveof both principal and any accrued interest*/function withdraw() public withdrawalDeadlineReached(true) claimDeadlineReached(false) notCompleted{require(balances[msg.sender] > 0, "You have no balance to withdraw!");uint256 individualBalance = balances[msg.sender];uint256 indBalanceRewards = individualBalance + ((block.timestamp-depositTimestamps[msg.sender])*rewardRatePerBlock);balances[msg.sender] = 0;
}
/*Allows any user to repatriate "unproductive" funds that are left in the staking contractpast the defined withdrawal period*/function execute() public claimDeadlineReached(true) notCompleted {uint256 contractBalance = address(this).balance;exampleExternalContract.complete{value: address(this).balance}();}
/*READ-ONLY function to calculate the time remaining before the minimum staking period has passed*/function withdrawalTimeLeft() public view returns (uint256 withdrawalTimeLeft) {if( block.timestamp >= withdrawalDeadline) {return (0);} else {return (withdrawalDeadline - block.timestamp);}}
/*READ-ONLY function to calculate the time remaining before the minimum staking period has passed*/function claimPeriodLeft() public view returns (uint256 claimPeriodLeft) {if( block.timestamp >= claimDeadline) {return (0);} else {return (claimDeadline - block.timestamp);}}
/*Time to "kill-time" on our local testnet*/function killTime() public {currentBlock = block.timestamp;}
/*\Function for our smart contract to receive ETHcc: https://docs.soliditylang.org/en/latest/contracts.html#receive-ether-function*/receive() external payable {emit Received(msg.sender, msg.value);}
}5. 进入前台! 我们刚刚经历了一堆 Solidity。
当涉及到前端显示时,Scaffold-Eth 试图让事情变得简单而美好。它包含了很多不同的反应组件,为用户提供了低代码的解决方案,以实现令人敬畏的 UI!我鼓励你玩玩这些组件。我鼓励你玩玩不同的组件,但与此同时,我们将在 spartan 方面学习更多。看看我们的 App.jsx 文件,特别是在链接 573 附近的代码块,我们看到一个代码块,用于从我们的 Solidity 合约中捕获发射的事件,并将其显示为一个列表。有效地,它允许我们记录从我们的智能合约发射的不同行动,解析存储的信息,然后直观地允许 dApp 用户查看他们的链上历史。虽然我们将通过在 Solidity 合约中发射事件来实践良好的编程标准,但这次,为了简单起见,我们将在前端忽略事件,所以让我们完全删除这个代码块。如果你看一下你的 Staker UI 标签,你会发现事件框已经被抹去了。前台编辑在大多数情况下,前台的许多代码将保持与默认的相同 在前面的步骤中,我们已经删除了事件反应组件。我们的最终目标是要有一个漂亮的、简单的用户界面。
请注意,默认的前台缺少一些默认 repo 的视觉元素。每秒奖励率为了构建这部分内容,我们在 Staker 合同块下直接插入以下代码。<div style={{ padding: 8, marginTop: 16 }}><div>Reward Rate Per Second:</div><Balance balance={rewardRatePerSecond} fontSize={64} /> ETH</div>
在这里,我们利用了 Scaffold-Eth 的平衡反应组件来帮助格式化!最后期限的 UI 元素为了构建截止日期视觉组件,我们使用了以下 2 段代码。// ** keep track of a variable from the contract in the local React state:const claimPeriodLeft = useContractReader(readContracts, "Staker", "claimPeriodLeft");console.log("⏳ Claim Period Left:", claimPeriodLeft);
const withdrawalTimeLeft = useContractReader(readContracts, "Staker", "withdrawalTimeLeft");console.log("⏳ Withdrawal Time Left:", withdrawalTimeLeft);<div style={{ padding: 8, marginTop: 16, fontWeight: "bold" }}><div>Claim Period Left:</div>{claimPeriodLeft && humanizeDuration(claimPeriodLeft.toNumber() * 1000)}</div>
<div style={{ padding: 8, marginTop: 16, fontWeight: "bold"}}><div>Withdrawal Period Left:</div>{withdrawalTimeLeft && humanizeDuration(withdrawalTimeLeft.toNumber() * 1000)}</div>
虽然第二个代码片断很熟悉,与我们已经看到的相似,但第一个代码块有点独特。不是说我们调用 claimPeriodLeft 和 withdrawalTimeLeft 来访问前端的存储变量值。然而,我们实际上必须先从智能合约中读取这些值本身。第一个代码片断处理了这个逻辑!杂项:对其他现有的 UI 组件的编辑现在你已经看到了 2 个不同的前台 UI 组件的例子,你能想出如何对前台进行其余的修改,使其看起来像上面提供的样本吗!?你应该只需要在这里调整一些参数,所以不要想得太多了对用户界面的自由发挥虽然 Scaffold-Eth 有很多默认组件,使用户能够简单地利用 "低代码 "解决方案,但它也使用户能够访问更大的前端库。默认情况下,它有一个与 Ant Design react components(https://ant.design/components/overview/)的挂钩,允许任何人从那里提取组件在我们的前端样本中,我们实际上看到了 "线 "来划分每个大块的视觉组件。通过探索 Ant Design 中的不同选项,可以重现这些线条如果你想得到提示,请看一下 Ant Dividers!不要忘记,我们需要在 App.jsx 文件的顶部导入我们计划使用的组件从 "antd "导入 { Alert, Button, Col, Menu, Row, List, Divider }。如果你已经跟上了前端的代码,你的 App.jsx 应该看起来像下面这样。import WalletConnectProvider from "@walletconnect/web3-provider";//import Torus from "@toruslabs/torus-embed"import WalletLink from "walletlink";import { Alert, Button, Col, Menu, Row, List, Divider } from "antd";import "antd/dist/antd.css";import React, { useCallback, useEffect, useState } from "react";import { BrowserRouter, Link, Route, Switch } from "react-router-dom";import Web3Modal from "web3modal";import "./App.css";import { Account, Address, Balance, Contract, Faucet, GasGauge, Header, Ramp, ThemeSwitch } from "./components";import { INFURA_ID, NETWORK, NETWORKS } from "./constants";import { Transactor } from "./helpers";import {useBalance,useContractLoader,useContractReader,useGasPrice,useOnBlock,useUserProviderAndSigner,} from "eth-hooks";import { useEventListener } from "eth-hooks/events/useEventListener";import { useExchangeEthPrice } from "eth-hooks/dapps/dex";// import Hints from "./Hints";import { ExampleUI, Hints, Subgraph } from "./views";
import { useContractConfig } from "./hooks";import Portis from "@portis/web3";import Fortmatic from "fortmatic";import Authereum from "authereum";import humanizeDuration from "humanize-duration";
const { ethers } = require("ethers");/*Welcome to scaffold-eth !
*/
/// What chain are your contracts deployed to?const targetNetwork = NETWORKS.localhost; // <------- select your target frontend network (localhost, rinkeby, xdai, mainnet)
// Sorry for all the console loggingconst DEBUG = true;const NETWORKCHECK = true;
// providersif (DEBUG) console.log(" Connecting to Mainnet Ethereum");// const mainnetProvider = getDefaultProvider("mainnet", { infura: INFURA_ID, etherscan: ETHERSCAN_KEY, quorum: 1 });// const mainnetProvider = new InfuraProvider("mainnet",INFURA_ID);//// attempt to connect to our own scaffold eth rpc and if that fails fall back to infura...// Using StaticJsonRpcProvider as the chainId won't change see https://github.com/ethers-io/ethers.js/issues/901const scaffoldEthProvider = navigator.onLine? new ethers.providers.StaticJsonRpcProvider("https://rpc.scaffoldeth.io:48544"): null;const poktMainnetProvider = navigator.onLine? new ethers.providers.StaticJsonRpcProvider("https://eth-mainnet.gateway.pokt.network/v1/lb/611156b4a585a20035148406",): null;const mainnetInfura = navigator.onLine? new ethers.providers.StaticJsonRpcProvider("https://mainnet.infura.io/v3/" + INFURA_ID): null;// ( ⚠️ Getting "failed to meet quorum" errors? Check your INFURA_ID
// Your local provider is usually pointed at your local blockchainconst localProviderUrl = targetNetwork.rpcUrl;// as you deploy to other networks you can set REACT_APP_PROVIDER=https://dai.poa.network in packages/react-app/.envconst localProviderUrlFromEnv = process.env.REACT_APP_PROVIDER ? process.env.REACT_APP_PROVIDER : localProviderUrl;if (DEBUG) console.log(" Connecting to provider:", localProviderUrlFromEnv);const localProvider = new ethers.providers.StaticJsonRpcProvider(localProviderUrlFromEnv);
// block explorer URLconst blockExplorer = targetNetwork.blockExplorer;
// Coinbase walletLink initconst walletLink = new WalletLink({appName: "coinbase",});
// WalletLink provideconst walletLinkProvider = walletLink.makeWeb3Provider(https://mainnet.infura.io/v3/${INFURA_ID}
, 1);
// Portis ID: 6255fb2b-58c8-433b-a2c9-62098c05ddc9/*Web3 modal helps us "connect" external wallets:*/const web3Modal = new Web3Modal({network: "mainnet", // Optional. If using WalletConnect on xDai, change network to "xdai" and add RPC info below for xDai chain.cacheProvider: true, // optionaltheme: "light", // optional. Change to "dark" for a dark theme.providerOptions: {walletconnect: {package: WalletConnectProvider, // requiredoptions: {bridge: "https://polygon.bridge.walletconnect.org",infuraId: INFURA_ID,rpc: {1: https://mainnet.infura.io/v3/${INFURA_ID}
, // mainnet // For more WalletConnect providers: https://docs.walletconnect.org/quick-start/dapps/web3-provider#required42: https://kovan.infura.io/v3/${INFURA_ID}
,100: "https://dai.poa.network", // xDai},},},portis: {display: {logo: "https://user-images.githubusercontent.com/9419140/128913641-d025bc0c-e059-42de-a57b-422f196867ce.png",name: "Portis",description: "Connect to Portis App",},package: Portis,options: {id: "6255fb2b-58c8-433b-a2c9-62098c05ddc9",},},fortmatic: {package: Fortmatic, // requiredoptions: {key: "pk_live_5A7C91B2FC585A17", // required},},// torus: {// package: Torus,// options: {// networkParams: {// host: "https://localhost:8545", // optional// chainId: 1337, // optional// networkId: 1337 // optional// },// config: {// buildEnv: "development" // optional// },// },// },"custom-walletlink": {display: {logo: "https://play-lh.googleusercontent.com/PjoJoG27miSglVBXoXrxBSLveV6e3EeBPpNY55aiUUBM9Q1RCETKCOqdOkX2ZydqVf0",name: "Coinbase",description: "Connect to Coinbase Wallet (not Coinbase App)",},package: walletLinkProvider,connector: async (provider, _options) => {await provider.enable();return provider;},},authereum: {package: Authereum, // required},},});
function App(props) {const mainnetProvider =poktMainnetProvider && poktMainnetProvider._isProvide? poktMainnetProvide: scaffoldEthProvider && scaffoldEthProvider._network? scaffoldEthProvide: mainnetInfura;
const [injectedProvider, setInjectedProvider] = useState();const [address, setAddress] = useState();
const logoutOfWeb3Modal = async () => {await web3Modal.clearCachedProvider();if (injectedProvider && injectedProvider.provider && typeof injectedProvider.provider.disconnect == "function") {await injectedProvider.provider.disconnect();}setTimeout(() => {window.location.reload();}, 1);};
/* This hook will get the price of ETH from Uniswap: */const price = useExchangeEthPrice(targetNetwork, mainnetProvider);
/* This hook will get the price of Gas from ⛽️ EtherGasStation */const gasPrice = useGasPrice(targetNetwork, "fast");// Use your injected provider from Metamask or if you don't have it then instantly generate a burner wallet.const userProviderAndSigner = useUserProviderAndSigner(injectedProvider, localProvider);const userSigner = userProviderAndSigner.signer;
useEffect(() => {async function getAddress() {if (userSigner) {const newAddress = await userSigner.getAddress();setAddress(newAddress);}}getAddress();}, [userSigner]);
// You can warn the user if you would like them to be on a specific networkconst localChainId = localProvider && localProvider._network && localProvider._network.chainId;const selectedChainId =userSigner && userSigner.provider && userSigner.provider._network && userSigner.provider._network.chainId;
// For more hooks, check out eth-hooks at: https://www.npmjs.com/package/eth-hooks
// The transactor wraps transactions and provides notificiationsconst tx = Transactor(userSigner, gasPrice);
// Faucet Tx can be used to send funds from the faucetconst faucetTx = Transactor(localProvider, gasPrice);
// scaffold-eth is full of handy hooks like this one to get your balance:const yourLocalBalance = useBalance(localProvider, address);
// Just plug in different providers to get your balance on different chains:const yourMainnetBalance = useBalance(mainnetProvider, address);
const contractConfig = useContractConfig();
// Load in your local contract and read a value from it:const readContracts = useContractLoader(localProvider, contractConfig);
// If you want to make write transactions to your contracts, use the userSigner:const writeContracts = useContractLoader(userSigner, contractConfig, localChainId);
// EXTERNAL CONTRACT EXAMPLE://// If you want to bring in the mainnet DAI contract it would look like:const mainnetContracts = useContractLoader(mainnetProvider, contractConfig);
// If you want to call a function on a new blockuseOnBlock(mainnetProvider, () => {console.log(⛓ A new mainnet block is here: ${mainnetProvider._lastBlockNumber}
);});
// Then read your DAI balance like:const myMainnetDAIBalance = useContractReader(mainnetContracts, "DAI", "balanceOf", ["0x34aA3F359A9D614239015126635CE7732c18fDF3",]);
//keep track of contract balance to know how much has been staked total:const stakerContractBalance = useBalance(localProvider,readContracts && readContracts.Staker ? readContracts.Staker.address : null,);if (DEBUG) console.log(" stakerContractBalance", stakerContractBalance);
const rewardRatePerSecond = useContractReader(readContracts, "Staker", "rewardRatePerSecond");console.log(" Reward Rate:", rewardRatePerSecond);
// ** keep track of a variable from the contract in the local React state:const balanceStaked = useContractReader(readContracts, "Staker", "balances", [address]);console.log(" balanceStaked:", balanceStaked);
// ** Listen for broadcast eventsconst stakeEvents = useEventListener(readContracts, "Staker", "Stake", localProvider, 1);console.log(" stake events:", stakeEvents);
const receiveEvents = useEventListener(readContracts, "Staker", "Received", localProvider, 1);console.log(" receive events:", receiveEvents);
// ** keep track of a variable from the contract in the local React state:const claimPeriodLeft = useContractReader(readContracts, "Staker", "claimPeriodLeft");console.log("⏳ Claim Period Left:", claimPeriodLeft);
const withdrawalTimeLeft = useContractReader(readContracts, "Staker", "withdrawalTimeLeft");console.log("⏳ Withdrawal Time Left:", withdrawalTimeLeft);
// ** Listen for when the contract has been 'completed'const complete = useContractReader(readContracts, "ExampleExternalContract", "completed");console.log("✅ complete:", complete);
const exampleExternalContractBalance = useBalance(localProvider,readContracts && readContracts.ExampleExternalContract ? readContracts.ExampleExternalContract.address : null,);if (DEBUG) console.log(" exampleExternalContractBalance", exampleExternalContractBalance);
let completeDisplay = "";if (complete) {completeDisplay = (<div style={{padding: 64, backgroundColor: "#eeffef", fontWeight: "bold", color: "rgba(0, 0, 0, 0.85)" }} >-- Staking App Fund Repatriation Executed --<Balance balance={exampleExternalContractBalance} fontSize={32} /> ETH locked!</div>);}
/*const addressFromENS = useResolveName(mainnetProvider, "austingriffith.eth");console.log(" Resolved austingriffith.eth as:", addressFromENS)*/
//// DEBUG //useEffect(() => {if (DEBUG &&mainnetProvider &&address &&selectedChainId &&yourLocalBalance &&yourMainnetBalance &&readContracts &&writeContracts &&mainnetContracts) {console.log("_____________________________________ scaffold-eth _____________________________________");console.log(" mainnetProvider", mainnetProvider);console.log(" localChainId", localChainId);console.log(" selected address:", address);console.log(" ♂️ selectedChainId:", selectedChainId);console.log(" yourLocalBalance", yourLocalBalance ? ethers.utils.formatEther(yourLocalBalance) : "...");console.log(" yourMainnetBalance", yourMainnetBalance ? ethers.utils.formatEther(yourMainnetBalance) : "...");console.log(" readContracts", readContracts);console.log(" DAI contract on mainnet:", mainnetContracts);console.log(" yourMainnetDAIBalance", myMainnetDAIBalance);console.log(" writeContracts", writeContracts);}}, [mainnetProvider,address,selectedChainId,yourLocalBalance,yourMainnetBalance,readContracts,writeContracts,mainnetContracts,]);
let networkDisplay = "";if (NETWORKCHECK && localChainId && selectedChainId && localChainId !== selectedChainId) {const networkSelected = NETWORK(selectedChainId);const networkLocal = NETWORK(localChainId);if (selectedChainId === 1337 && localChainId === 31337) {networkDisplay = (<div style={{ zIndex: 2, position: "absolute", right: 0, top: 60, padding: 16 }}><Alertmessage="⚠️ Wrong Network ID"description={<div>You have <b>chain id 1337</b> for localhost and you need to change it to <b>31337</b> to work withHardHat.<div>(MetaMask -> Settings -> Networks -> Chain ID -> 31337)</div></div>}type="error"closable={false}/></div>);} else {networkDisplay = (<div style={{ zIndex: 2, position: "absolute", right: 0, top: 60, padding: 16 }}><Alertmessage="⚠️ Wrong Network"description={<div>You have <b>{networkSelected && networkSelected.name}</b> selected and you need to be on{" "}<ButtononClick={async () => {const ethereum = window.ethereum;const data = [{chainId: "0x" + targetNetwork.chainId.toString(16),chainName: targetNetwork.name,nativeCurrency: targetNetwork.nativeCurrency,rpcUrls: [targetNetwork.rpcUrl],blockExplorerUrls: [targetNetwork.blockExplorer],},];console.log("data", data);
} else {networkDisplay = (<div style={{ zIndex: -1, position: "absolute", right: 154, top: 28, padding: 16, color: targetNetwork.color }}>{targetNetwork.name}</div>);}
const loadWeb3Modal = useCallback(async () => {const provider = await web3Modal.connect();setInjectedProvider(new ethers.providers.Web3Provider(provider));
}, [setInjectedProvider]);
useEffect(() => {if (web3Modal.cachedProvider) {loadWeb3Modal();}}, [loadWeb3Modal]);
const [route, setRoute] = useState();useEffect(() => {setRoute(window.location.pathname);}, [setRoute]);
let faucetHint = "";const faucetAvailable = localProvider && localProvider.connection && targetNetwork.name.indexOf("local") !== -1;
const [faucetClicked, setFaucetClicked] = useState(false);if (!faucetClicked &&localProvider &&localProvider._network &&localProvider._network.chainId === 31337 &&yourLocalBalance &ðers.utils.formatEther(yourLocalBalance) <= 0) {faucetHint = (<div style={{ padding: 16 }}><Buttontype="primary"onClick={() => {faucetTx({to: address,value: ethers.utils.parseEther("0.01"),});setFaucetClicked(true);}}>Grab funds from the faucet ⛽️</Button></div>);}
return (<div className="App">{/* ✏️ Edit the header and change the title to your project name */}<Header />{networkDisplay}<BrowserRouter><Menu style={{ textAlign: "center" }} selectedKeys={[route]} mode="horizontal"><Menu.Item key="/"><LinkonClick={() => {setRoute("/");}}to="/">Staker UI</Link></Menu.Item><Menu.Item key="/contracts"><LinkonClick={() => {setRoute("/contracts");}}to="/contracts">Debug Contracts</Link></Menu.Item></Menu>
);}
export default App。
在开发者环境、Solidity 和前端代码方面,我们已经一起完成了很多新的组件。验证您的 dApp 的功能是否符合预期!
dApp 是否具有单次使用押金的功能?
提款/资金返还条件是否得到尊重?继续点击 yarn deploy --reset 几次,检查每个时间窗口。
更新 Staker.sol 合约中的利息机制,这样你就可以根据存款和提款之间的区块获得 "非线性 "的 ETH 数额我建议实现一个基本的指数函数!1. 允许用户向智能合约存入任意数量的 ETH,而不仅仅是 0.5ETH。
不要使用 vanilla ExampleExternalContract 合约,在 Staker.sol 中实现一个函数,允许你取回锁定在 ExampleExternalContract 中的 ETH,并将其重新存入 Staker 合约中。• 请确保只有 "白名单 "中的一个地址可以调用这个新函数,以控制其使用。• 确保你创建了逻辑/删除了现有的代码,以确保用户能够一次又一次地与 Staker 合同互动 我们希望能够反复地从 Staker -> ExampleExternalContract 进行 ping-pong。
引用链接[1] Scaffold-eth: http://scaffoldeth.io/[2] SpeedRunEthereum: https://speedrunethereum.com/[3] Challenge #1: https://speedrunethereum.com/challenge/decentralized-staking[4] SpeedRunEthereum: https://speedrunethereum.com/[5] Challenge #1: https://speedrunethereum.com/challenge/decentralized-staking[6] 准则: https://docs.alchemy.com/docs/how-to-build-a-staking-dapp#project-parameters[7] 公共变量部分: https://docs.alchemy.com/docs/how-to-build-a-staking-dapp#public-variables[8] Solidity By Example: https://solidity-by-example.org/function-modifier[9] 链接 573 附近的代码块: https://github.com/scaffold-eth/scaffold-eth/blob/challenge-1-decentralized-staking/packages/react-app/src/App.jsx#L573
评论