使用Foundry用Solidity编写一个Stake质押的项目

  • 链创通
  • 更新于 2024-07-24 22:21
  • 阅读 1063

在Foundry中用Solidity编写一个质押挖矿的项目

在foundry中用Solidity编写一个质押挖矿的项目,实现如下功能:

  1. 用户随时可以质押项目方代币 RNT(自定义的ERC20+ERC2612) ,开始赚取项目方Token(esRNT);
  2. 可随时解押提取已质押的 RNT;
  3. 可随时领取esRNT奖励,每质押1个RNT每天可奖励 1 esRNT;
  4. esRNT 是锁仓性的 RNT, 1 esRNT 在 30 天后可兑换 1 RNT,随时间线性释放,支持提前将 esRNT 兑换成 RNT,但锁定部分将被 burn 燃烧掉。

我们需要三个合约:

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 合约。

1. 安装 Foundry

首先,确保你已经安装了 Foundry,工具的安装使用,请参考官网的官方文档:https://getfoundry.sh

curl -L https://foundry.paradigm.xyz | bash
foundryup

2. 创建项目

创建一个新的 Foundry 项目:

forge init stakeRNT
cd stakeRNT

3. 添加依赖

如果需要使用 OpenZeppelin 库,可以添加依赖:

forge install OpenZeppelin/openzeppelin-contracts

1. RNT 合约

在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

2. esRNT 合约

esRNT.sol 合约是一个锁仓性的 RNT 合约,包含 mint 和 burn 函数。它会记录每个锁仓的记录,并支持线性释放。

1、传进来的代币和要生成的代币之间是什么关系?

2、传进来怎么产生锁仓与解锁的关系?

用户锁仓就要开始挖币,同时的。我们要定义一个locks的集合,集合里有结构体记录锁仓的信息,锁仓的之后要对锁仓的顺序进行记录,触发事件,然后在上面对触发的事件进行定义。

函数从设置各种可能的限定条件、 写参数,定义参数,到实现业务逻辑的行动,到触发行动,到记录行动的事件,是按照合约开发的逻辑进行。

在这个质押挖矿项目中,RNTesRNT 合约在技术上有以下关系和作用:

RNT 合约

  • 类型:标准 ERC20 代币合约,具有 ERC2612 标准(允许通过签名进行许可)。

  • 作用RNT 是项目方的基础代币,用户可以质押 RNT 来赚取 esRNT

  • 功能

    • ERC20:实现标准的 ERC20 功能,如转账、查询余额等。
    • ERC20Permit:允许通过签名进行许可的功能。
    • Ownable:添加了所有者的管理功能。

esRNT 合约

  • 类型:扩展的 ERC20 合约,用于锁仓 RNT
  • 作用esRNT 是锁仓性的 RNT,1 个 esRNT 在 30 天后可兑换 1 个 RNT,支持线性释放。
  • 功能:
    • 锁仓功能:esRNT 代币代表锁仓的 RNT,并且需要记录每个锁仓的用户、数量和锁仓时间。
    • mint:项目方可以铸造 esRNT,用于奖励用户。
    • burn:用户可以将 esRNT 兑换回 RNT,支持线性释放和提前赎回。

技术关系

  1. 合约之间的依赖
    • esRNT 合约需要知道 RNT 合约的地址,以便在 burn 时将锁仓的 RNT 返还给用户。
    • StakePool 合约在用户质押 RNT 时,会记录质押信息,并在用户领取奖励时铸造 esRNT
  2. 交互流程
    • 用户质押 RNTStakePool 合约,合约记录质押信息。
    • 用户可以随时领取 esRNT 奖励,StakePool 合约调用 esRNT 合约的 mint 函数铸造奖励。
    • 用户可以在 30 天后通过 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

3. stakePool 合约

新建一个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)

部署步骤

  1. 部署 StakePool 合约
    • 选择 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_ADDRESSESRNT_ADDRESS

RNT_ADDRESS=<your_RNT_contract_address>
ESRNT_ADDRESS=<your_esRNT_contract_address>
  1. 运行部署脚本
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 中进行部署和测试。

点赞 0
收藏 3
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
链创通
链创通
0x5312...1e69
歪脖山徒步虾