From fb0eb52f257521d51ef906e57b00f473131f7794 Mon Sep 17 00:00:00 2001 From: CounterFire2023 <136581895+CounterFire2023@users.noreply.github.com> Date: Mon, 13 Jan 2025 17:42:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=AE=80=E5=8D=95=E7=9A=84?= =?UTF-8?q?=E8=B4=A8=E6=8A=BC=E5=90=88=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/staking/SimpleStake.sol | 92 +++++++++++++++++++++++++++ deploy/12_deploy_simple_stake.ts | 29 +++++++++ test/testSimpleStake.ts | 101 ++++++++++++++++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 contracts/staking/SimpleStake.sol create mode 100644 deploy/12_deploy_simple_stake.ts create mode 100644 test/testSimpleStake.ts diff --git a/contracts/staking/SimpleStake.sol b/contracts/staking/SimpleStake.sol new file mode 100644 index 0000000..71eab1e --- /dev/null +++ b/contracts/staking/SimpleStake.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +contract SimpleStake is Ownable, ReentrancyGuard { + using SafeERC20 for IERC20; + IERC20 public stakingToken; + uint256 public startTime; + uint256 public endTime; + uint256 public maxStakeAmount; + uint256 public totalStaked; + + struct Stake { + uint256 amount; + uint256 stakeTime; + bool unlocked; + } + + mapping(address account => Stake[] stakes) public stakes; + + event StakeCreated(address indexed user, uint256 amount, uint256 stakeId); + event Unlocked(address indexed user, uint256 amount); + event PeriodUpdated(uint256 startTime, uint256 endTime); + event MaxStakeAmountUpdated(uint256 maxStakeAmount); + + constructor(IERC20 _stakingToken, uint256 _startTime, uint256 _endTime, uint256 _maxStakeAmount) { + require(_endTime > _startTime, "End time must be after start time"); + stakingToken = _stakingToken; + startTime = _startTime; + endTime = _endTime; + maxStakeAmount = _maxStakeAmount; + } + + function stake(uint256 _amount) public nonReentrant { + require(block.timestamp >= startTime, "Staking has not started yet"); + require(block.timestamp <= endTime, "Staking has ended"); + require(_amount > 0, "Amount must be greater than 0"); + require(_amount <= maxStakeAmount, "Amount exceeds max stake amount"); + require(totalStaked + _amount <= maxStakeAmount, "Exceeds max stake amount"); + address user = _msgSender(); + totalStaked += _amount; + stakingToken.safeTransferFrom(user, address(this), _amount); + + stakes[user].push(Stake(_amount, block.timestamp, false)); + emit StakeCreated(user, _amount, stakes[user].length - 1); + } + + /** + * @dev Unstake all stakes that have not been unlocked yet + */ + function unstake() public nonReentrant { + require( + block.timestamp > endTime, + "Unlock time not reached" + ); + address user = _msgSender(); + require(stakes[user].length > 0, "No stakes to unstake"); + + uint256 totalAmount = 0; + for (uint256 i = 0; i < stakes[user].length; i++) { + Stake storage stakeInfo = stakes[user][i]; + if (!stakeInfo.unlocked) { + totalAmount += stakeInfo.amount; + stakeInfo.unlocked = true; + } + } + require(totalAmount > 0, "No stakes to unstake"); + totalStaked -= totalAmount; + emit Unlocked(user, totalAmount); + stakingToken.safeTransfer(user, totalAmount); + } + + function setTime(uint256 _startTime, uint256 _endTime) public onlyOwner { + require(_endTime > _startTime, "End time must be after start time"); + startTime = _startTime; + endTime = _endTime; + emit PeriodUpdated(_startTime, _endTime); + } + + function setMaxStakeAmount(uint256 _maxStakeAmount) public onlyOwner { + maxStakeAmount = _maxStakeAmount; + emit MaxStakeAmountUpdated(_maxStakeAmount); + } + + function userStakes(address _user) public view returns (Stake[] memory) { + return stakes[_user]; + } +} diff --git a/deploy/12_deploy_simple_stake.ts b/deploy/12_deploy_simple_stake.ts new file mode 100644 index 0000000..b197fba --- /dev/null +++ b/deploy/12_deploy_simple_stake.ts @@ -0,0 +1,29 @@ +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { DeployFunction } from "hardhat-deploy/types"; +import { deplayOne, updateArray } from "../scripts/utils" +import { expandDecimals } from "../test/shared/utilities"; + + +const deployTokenMall: DeployFunction = + async function (hre: HardhatRuntimeEnvironment) { + const provider = hre.ethers.provider; + const from = await (await provider.getSigner()).getAddress(); + const config = require(`../config/config_${hre.network.name}`); + const {cec} = config.staking; + + const startTime = Date.now() / 1000 | 0; + const endTime = startTime + 2 * 24 * 60 * 60; + const simpleStake = await deplayOne({ + hre, + name: "simpleStake", + type: "logic", + contractName: "SimpleStake", + args: [cec, startTime, endTime, expandDecimals(20000000, 18)], + verify: true, + }); + + }; + + deployTokenMall.tags = ["SimpleStake"]; + +export default deployTokenMall; diff --git a/test/testSimpleStake.ts b/test/testSimpleStake.ts new file mode 100644 index 0000000..00ba0c1 --- /dev/null +++ b/test/testSimpleStake.ts @@ -0,0 +1,101 @@ +import { expect } from 'chai' +import hre from "hardhat"; +import { + ZeroAddress, + getBytes, + solidityPackedKeccak256, + formatEther, + JsonRpcProvider, +} from 'ethers' + +import { + loadFixture, +} from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import { expandDecimals, increaseTime, mineBlock, reportGasUsed } from './shared/utilities'; +const secondsOneDay = BigInt(24 * 60 * 60); +const secondsPerYear = 365 * 24 * 60 * 60; +// 1.5 / (365*24*60*60) +const rewardPerSecond = BigInt(1.5 * 10 ** 18) / BigInt(secondsPerYear) ; +// 47564688000 +console.log('rewardPerSecond: ', rewardPerSecond.toString()) + +const blockTime = async (provider: any) => { + const block = await provider.getBlock('latest') + return block ? block.timestamp : 0 +} + +describe('SimpleStake', function() { + async function deployOneContract() { + // Contracts are deployed using the first signer/account by default + const [owner, user0, user1, user2] = await hre.ethers.getSigners(); + const chainId = hre.network.config.chainId + + const Cec = await hre.ethers.getContractFactory("MintableBaseToken"); + const cec = await Cec.deploy("test cec", "cec"); + + const SimpleStake = await hre.ethers.getContractFactory("SimpleStake"); + const startTime = await blockTime(owner.provider) + const endTime = startTime + 14 * 24 * 60 * 60 + const maxStakeAmount = expandDecimals(1000, 18) + const simpleStake = await SimpleStake.deploy( + cec.target, + startTime, + endTime, + maxStakeAmount + ); + await cec.setMinter(owner.address, true) + + return { owner, user0, user1, user2, chainId, cec, simpleStake, startTime, endTime }; + } + + describe("Staking CEC", function () { + it("stake unstake", async () => { + const {owner, user0, user1, user2, cec, simpleStake, startTime, endTime} = await loadFixture(deployOneContract); + const wallet = owner + const provider = wallet.provider; + await cec.setMinter(wallet.address, true) + await cec.mint(user0.address, expandDecimals(1500, 18)) + expect(await cec.balanceOf(user0.address)).eq(expandDecimals(1500, 18)) + + // @ts-ignore + await cec.connect(user0).approve(simpleStake.target, expandDecimals(1001, 18)) + + // @ts-ignore + await expect(simpleStake.connect(user0).stake(expandDecimals(1100, 18))) + .to.be.revertedWith("Amount exceeds max stake amount") + // @ts-ignore + await simpleStake.connect(user0).stake(expandDecimals(800, 18)) + let _startTime = await blockTime(provider) + console.log('startTime: ', _startTime) + expect(await cec.balanceOf(user0.address)).eq(expandDecimals(700, 18)) + + // @ts-ignore + await expect(simpleStake.connect(user0).stake(expandDecimals(300, 18))) + .to.be.revertedWith("Exceeds max stake amount") + // @ts-ignore + let stakeOne = await simpleStake.connect(user0).stakes(user0.address, 0) + console.log('stakes: ', stakeOne) + // @ts-ignore + let stakes = await simpleStake.connect(user0).userStakes(user0.address) + console.log('stakes: ', stakes) + let nowTime = await blockTime(provider) + console.log('nowTime: ', nowTime) + + // @ts-ignore + await expect(simpleStake.connect(user0).unstake()) + .to.be.revertedWith("Unlock time not reached") + const period = endTime - startTime + await increaseTime(provider, period ) + await mineBlock(provider) + // @ts-ignore + await simpleStake.connect(user0).unstake() + expect(await cec.balanceOf(user0.address)).eq(expandDecimals(1500, 18)) + // @ts-ignore + await expect(simpleStake.connect(user0).unstake()) + .to.be.revertedWith("No stakes to unstake") + + + + }) + }) +}) \ No newline at end of file