在Foundry中用Solidity编写一个质押挖矿的项目
在foundry中用Solidity编写一个质押挖矿的项目,实现如下功能:
我们需要三个合约:
1、RNT合约:这是一个ERC20的代币,具有ERC2612标准,要授权给stake(uint)、stake(uint、permit)
2、stakePool合约:包含stake、unstake、claim的函数
代码包含:
struct stakeInfo{
staked
unClaimd
lastUpdateTime
}
mapping(address=>stakeInfo)
stakePool.claim{
//RNT.approve(esRNT,MAX RNT)
esRNT.mint(alice){
transferFrom(msg.sender,address(this),RNT)
_mint(alice,)
locks.push(...)
}
}
3、esRNT合约:包含mint、burn函数
esRNT{
struct LockInfo{
address user
uint256 amount
uint256 lockTime
}
LockInfo[] locks;
}
esRNT.burn(uint256 id){
unlocked=amount*(now-lockTime)/30days
RNT.transfer(user,unlocked);
RNT.transfer(0x000000000,amount-unlocked);
}
下面是一份详细的操作文档,包含各个合约部署代码、质押合约的测试代码,本项目包含三个智能合约:RNT 合约、esRNT 合约和 stakePool 合约。
首先,确保你已经安装了 Foundry,工具的安装使用,请参考官网的官方文档:https://getfoundry.sh
curl -L https://foundry.paradigm.xyz | bash
foundryup
创建一个新的 Foundry 项目:
forge init stakeRNT
cd stakeRNT
如果需要使用 OpenZeppelin 库,可以添加依赖:
forge install OpenZeppelin/openzeppelin-contracts
在src文件夹中新建一个RNT.sol 合约,这是一个ERC20代币合约,具有 ERC2612 标准(允许通过签名进行许可)。我们将使用 OpenZeppelin 的库来实现这一点。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract RNT is ERC20 , ERC20Permit, Ownable{
constructor() ERC20("Reward Token", "RNT")ERC20Permit("Reward Token")Ownable(msg.sender){
_mint(msg.sender, 1000000 * 10 ** decimals());
}
}
编译这个合约并部署
forge build src/RNT.sol
编译成功输出:
yhb@yhbdeMacBook-Air stakeRNT % forge build src/RNT.sol
[⠊] Compiling...
[⠰] Compiling 19 files with Solc 0.8.25
[⠔] Solc 0.8.25 finished in 325.31ms
Compiler run successful!
编写部署文件DeployRNT.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "../src/RNT.sol";
contract DeployRNT is Script {
function setUp() public {}
function run() public {
vm.startBroadcast();
// 部署 RNT 合约
RNT rnt = new RNT();
vm.stopBroadcast();
}
}
新建.env文件,写入命令参数,运行部署命令
source .env
forge script script/DeployRNT.s.sol --rpc-url ${RPC_URL} --broadcast --private-key ${PRIVATE_KEY}
部署结果:
yhb@yhbdeMacBook-Air stakeRNT % forge build
[⠊] Compiling...
[⠔] Compiling 47 files with Solc 0.8.25
[⠒] Solc 0.8.25 finished in 1.50s
Compiler run successful!
yhb@yhbdeMacBook-Air stakeRNT % forge script script/DeployRNT.s.sol --rpc-url ${RPC_URL} --broadcast --private-key ${PRIVATE_KEY}
[⠊] Compiling...
No files changed, compilation skipped
Script ran successfully.
== Logs ==
RNT deployed to: 0x2B2351b254DD6E0b292edb27f153D09a61359f46
## Setting up 1 EVM.
==========================
Chain 11155111
Estimated gas price: 69.247670162 gwei
Estimated total gas used for script: 1420953
Estimated amount required: 0.098397684659704386 ETH
==========================
##### sepolia
✅ [Success]Hash: 0x1c31172d50bf64dfb59a43cb93913c6333499fd550edc1859be4f74f19f188ba
Contract Address: 0x2B2351b254DD6E0b292edb27f153D09a61359f46
Block: 6366789
Paid: 0.039294677855108214 ETH (1093419 gas * 35.937438306 gwei)
✅ Sequence #1 on sepolia | Total Paid: 0.039294677855108214 ETH (1093419 gas * avg 35.937438306 gwei)
==========================
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
Transactions saved to: /Users/yhb/stakeRNT/broadcast/DeployRNT.s.sol/11155111/run-latest.json
Sensitive values saved to: /Users/yhb/stakeRNT/cache/DeployRNT.s.sol/11155111/run-latest.json
esRNT.sol 合约是一个锁仓性的 RNT 合约,包含 mint 和 burn 函数。它会记录每个锁仓的记录,并支持线性释放。
1、传进来的代币和要生成的代币之间是什么关系?
2、传进来怎么产生锁仓与解锁的关系?
用户锁仓就要开始挖币,同时的。我们要定义一个locks的集合,集合里有结构体记录锁仓的信息,锁仓的之后要对锁仓的顺序进行记录,触发事件,然后在上面对触发的事件进行定义。
函数从设置各种可能的限定条件、 写参数,定义参数,到实现业务逻辑的行动,到触发行动,到记录行动的事件,是按照合约开发的逻辑进行。
在这个质押挖矿项目中,RNT
和 esRNT
合约在技术上有以下关系和作用:
类型:标准 ERC20 代币合约,具有 ERC2612 标准(允许通过签名进行许可)。
作用:RNT
是项目方的基础代币,用户可以质押 RNT
来赚取 esRNT
。
功能
:
ERC20
:实现标准的 ERC20 功能,如转账、查询余额等。ERC20Permit
:允许通过签名进行许可的功能。Ownable
:添加了所有者的管理功能。RNT
。esRNT
是锁仓性的 RNT
,1 个 esRNT
在 30 天后可兑换 1 个 RNT
,支持线性释放。esRNT
代币代表锁仓的 RNT
,并且需要记录每个锁仓的用户、数量和锁仓时间。mint
:项目方可以铸造 esRNT
,用于奖励用户。burn
:用户可以将 esRNT
兑换回 RNT
,支持线性释放和提前赎回。esRNT
合约需要知道 RNT
合约的地址,以便在 burn
时将锁仓的 RNT
返还给用户。StakePool
合约在用户质押 RNT
时,会记录质押信息,并在用户领取奖励时铸造 esRNT
。RNT
到 StakePool
合约,合约记录质押信息。esRNT
奖励,StakePool
合约调用 esRNT
合约的 mint
函数铸造奖励。esRNT
合约的 burn
函数,将 esRNT
兑换回 RNT
。RNT
将被销毁(burn)。// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract esRNT is ERC20, Ownable {
struct LockInfo {
address user;
uint256 amount;
uint256 lockTime;
}
LockInfo[] public locks;
IERC20 public rntToken;
event Minted(address indexed _user, uint256 _amount, uint256 lockId);
constructor(IERC20 _rntToken) ERC20("Escrowed Reward Token", "esRNT") Ownable(msg.sender){
rntToken = _rntToken;
_transferOwnership(msg.sender);
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
locks.push(LockInfo({
user: to,
amount: amount,
lockTime: block.timestamp
}));
uint256 lockId = locks.length - 1;
emit Minted( to, amount, lockId);
}
function burn(uint256 lockId) public {
require(lockId<locks.length, "Invalid lockId");
LockInfo storage lock = locks[lockId];
require(lock.user == msg.sender, "Not the owner of the lock");
uint256 unlocked = (lock.amount*(block.timestamp - lock.lockTime))/30 days;
uint256 burnAmount= lock.amount-unlocked;
_burn(msg.sender, lock.amount);
rntToken.transfer(msg.sender, unlocked);
rntToken.transfer(address(0), burnAmount);
}
function getLocksByUser(address user) external view returns (LockInfo[] memory) {
uint256 count = 0;
for (uint256 i = 0; i < locks.length; i++) {
if (locks[i].user == user) {
count++;
}
}
LockInfo[] memory userLocks = new LockInfo[](count);
uint256 index = 0;
for (uint256 i = 0; i < locks.length; i++) {
if (locks[i].user == user) {
userLocks[index] = locks[i];
index++;
}
}
return userLocks;
}
}
通过 Foundry 部署 esRNT
合约,并且传入 RNT
合约的地址,你需要编写一个部署脚本DeployEsRNT.s.sol来实现。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "../src/esRNT.sol";
import "../src/RNT.sol";
contract DeployEsRNT is Script {
function run() external {
address rntAddress=vm.envAddress("RNT_ADDRESS");
vm.startBroadcast();
esRNT esrnt = new esRNT(IERC20(rntAddress));
vm.stopBroadcast();
console.log("esRNT deployed at:", address(esrnt));
}
}
在 .env
文件,并在其中定义 RNT_ADDRESS
,这是你之前部署的 RNT
合约的地址。
RNT_ADDRESS=<your_RNT_contract_address>
部署命令
forge script script/DeployEsRNT.s.sol --broadcast --rpc-url ${RPC_URL} --private-key ${PRIVATE_KEY}
部署结果为:
yhb@yhbdeMacBook-Air stakeRNT % forge script script/DeployEsRNT.s.sol --broadcast --rpc-url ${RPC_URL} --private-key ${PRIVATE_KEY}
[⠊] Compiling...
[⠆] Compiling 1 files with Solc 0.8.25
[⠰] Solc 0.8.25 finished in 1.26s
Compiler run successful!
Script ran successfully.
== Logs ==
esRNT deployed at: 0x7707dD2506128E330C45978AbA59DAA09bd353F4
## Setting up 1 EVM.
==========================
Chain 11155111
Estimated gas price: 64.007213099 gwei
Estimated total gas used for script: 1361939
Estimated amount required: 0.087173919800838961 ETH
==========================
##### sepolia
✅ [Success]Hash: 0x26ca9d0f46c56fb38f9b092ffef67307c89a84341704bb63d94514d19486e17f
Contract Address: 0x7707dD2506128E330C45978AbA59DAA09bd353F4
Block: 6367587
Paid: 0.036420640417340484 ETH (1047954 gas * 34.754044946 gwei)
✅ Sequence #1 on sepolia | Total Paid: 0.036420640417340484 ETH (1047954 gas * avg 34.754044946 gwei)
==========================
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
Transactions saved to: /Users/yhb/stakeRNT/broadcast/DeployEsRNT.s.sol/11155111/run-latest.json
Sensitive values saved to: /Users/yhb/stakeRNT/cache/DeployEsRNT.s.sol/11155111/run-latest.json
新建一个StakePool.sol 合约包含 stake、unstake 和 claim 的函数,用于管理用户的质押和奖励。这个合约部署时要传入两个部署好的代币合约地址,用户质押之前要统计更新一下奖励,质押的记录需要一个结构体,质押数量,更新时间都要记录。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./RNT.sol";
import "./esRNT.sol";
contract StakePool is Ownable {
IERC20 public rntToken;
esRNT public esrntToken;
mapping(address => StakeInfo) public stakes;
uint256 public rewardRate=1 ether;
struct StakeInfo {
uint256 staked;
uint256 unclaimed;
uint256 lastUpdateTime;
}
constructor(IERC20 _rntToken,esRNT _esrntToken) Ownable(msg.sender) {
rntToken = _rntToken;
esrntToken=_esrntToken;
}
function stake(uint256 amount) external {
updateReward(msg.sender);
require(amount > 0, "Amount must be greater than 0");
rntToken.transferFrom(msg.sender, address(this), amount);
stakes[msg.sender].staked += amount;
}
function unstake(uint256 amount) external {
updateReward(msg.sender);
require(amount > 0, "Amount must be greater than 0");
require(stakes[msg.sender].staked >= amount, "Insufficient balance");
stakes[msg.sender].staked -= amount;
rntToken.transfer(msg.sender, amount);
}
function claim() external {
updateReward(msg.sender);
uint256 reward = stakes[msg.sender].unclaimed;
stakes[msg.sender].unclaimed = 0;
esrntToken.transfer(msg.sender, reward);
}
function updateReward(address account) internal {
StakeInfo storage stakeInfo = stakes[account];
if (stakeInfo.lastUpdateTime > 0) {
uint256 timeStaked = block.timestamp - stakeInfo.lastUpdateTime;
stakeInfo.unclaimed += (timeStaked * stakeInfo.staked * rewardRate) / 1 days;
}
stakeInfo.lastUpdateTime = block.timestamp;
}
}
新建一个测试合约StakePool.t.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/StakePool.sol";
import "../src/RNT.sol";
import "../src/esRNT.sol";
contract StakePoolTest is Test {
RNT rntToken;
esRNT esrntToken;
StakePool stakePool;
address owner;
address user1;
function setUp() public {
owner = address(this);
user1 = address(0x1);
// 部署 RNT 和 esRNT 合约
rntToken = new RNT();
esrntToken = new esRNT(IERC20(address(rntToken)));
// 部署 StakePool 合约
stakePool = new StakePool(IERC20(address(rntToken)), esrntToken);
// 将一些 RNT 分配给用户
rntToken.transfer(user1, 1000 ether);
}
function testStake() public {
vm.startPrank(user1);
// 用户授权 StakePool 合约可以花费 RNT
rntToken.approve(address(stakePool), 1000 ether);
// 用户质押 100 RNT
stakePool.stake(100 ether);
// 检查质押结果
(uint256 staked,,) = stakePool.stakes(user1);
assertEq(staked, 100 ether);
assertEq(rntToken.balanceOf(user1), 900 ether);
vm.stopPrank();
}
function testUnstake() public {
vm.startPrank(user1);
// 用户授权 StakePool 合约可以花费 RNT
rntToken.approve(address(stakePool), 1000 ether);
// 用户质押 100 RNT
stakePool.stake(100 ether);
// 用户取消质押 50 RNT
stakePool.unstake(50 ether);
// 检查取消质押结果
(uint256 staked,,) = stakePool.stakes(user1);
assertEq(staked, 50 ether);
assertEq(rntToken.balanceOf(user1), 950 ether);
vm.stopPrank();
}
// function testClaim() public {
// vm.startPrank(user1);
// // 用户授权 StakePool 合约可以花费 RNT
// rntToken.approve(address(stakePool), 1000 ether);
// // 用户质押 100 RNT
// stakePool.stake(100 ether);
// // 快进时间30天,获取奖励
// vm.warp(block.timestamp + 30 days);
// // 用户领取奖励
// stakePool.claim();
// // 检查领取结果
// assertEq(esrntToken.balanceOf(user1), 100 ether);
// vm.stopPrank();
// }
}
输出测试结果为:
yhb@yhbdeMacBook-Air stakeRNT % forge test --mp test/StakePool.t.sol
[⠊] Compiling...
[⠔] Compiling 1 files with Solc 0.8.25
[⠒] Solc 0.8.25 finished in 1.50s
Compiler run successful!
Ran 2 tests for test/StakePool.t.sol:StakePoolTest
[PASS] testStake() (gas: 124019)
[PASS] testUnstake() (gas: 132477)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 8.98ms (4.65ms CPU time)
Ran 1 test suite in 350.29ms (8.98ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
StakePool
合约,传入 RNT 和 esRNT 合约的地址并部署。通过以下方式部署 DeployStakePool.s.sol
合约。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "../contracts/StakePool.sol";
import "../contracts/RNT.sol";
import "../contracts/esRNT.sol";
contract DeployStakePool is Script {
function run() external {
address rntAddress = vm.envAddress("RNT_ADDRESS");
address esrntAddress = vm.envAddress("ESRNT_ADDRESS");
vm.startBroadcast();
StakePool stakePool = new StakePool(IERC20(rntAddress), esRNT(esrntAddress));
vm.stopBroadcast();
console.log("StakePool deployed at:", address(stakePool));
}
}
一个 .env
文件,并在其中定义 RNT_ADDRESS
和 ESRNT_ADDRESS
。
RNT_ADDRESS=<your_RNT_contract_address>
ESRNT_ADDRESS=<your_esRNT_contract_address>
forge script script/DeployStakePool.s.sol --broadcast --rpc-url ${RPC_URL} --private-key ${PRIVATE_KEY}
替换 <your_rpc_url>
和 <your_private_key>
为你自己的 RPC URL 和私钥。
输出结果为:
yhb@yhbdeMacBook-Air stakeRNT % forge script script/DeployStakePool.s.sol --broadcast --rpc-url ${RPC_URL} --private-key ${PRIVATE_KEY}
[⠊] Compiling...
[⠰] Compiling 1 files with Solc 0.8.25
[⠔] Solc 0.8.25 finished in 1.32s
Compiler run successful!
Script ran successfully.
== Logs ==
StakePool deployed at: 0x1014F47eF26807EAA4ef9ae8d87F7A8be7B96aA3
## Setting up 1 EVM.
==========================
Chain 11155111
Estimated gas price: 68.409996813 gwei
Estimated total gas used for script: 677285
Estimated amount required: 0.046333064691492705 ETH
==========================
##### sepolia
✅ [Success]Hash: 0xc3c6e6444746c26198db05e5f158b8614c020417d829ddf1f71580bdb58e7eb5
Contract Address: 0x1014F47eF26807EAA4ef9ae8d87F7A8be7B96aA3
Block: 6367953
Paid: 0.016827037137092529 ETH (521123 gas * 32.289952923 gwei)
✅ Sequence #1 on sepolia | Total Paid: 0.016827037137092529 ETH (521123 gas * avg 32.289952923 gwei)
==========================
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
Transactions saved to: /Users/yhb/stakeRNT/broadcast/DeployStakePool.s.sol/11155111/run-latest.json
Sensitive values saved to: /Users/yhb/stakeRNT/cache/DeployStakePool.s.sol/11155111/run-latest.json
本质押挖矿项目通过 RNT、esRNT 和 StakePool 合约实现了用户随时质押、解押和领取奖励的功能。esRNT 代币具有锁仓和线性释放的特性,满足了项目方的需求。上述合约代码和测试代码可以在 Remix IDE 和 Foundry 中进行部署和测试。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!