增加简单的质押合约
This commit is contained in:
parent
67a25b6850
commit
fb0eb52f25
92
contracts/staking/SimpleStake.sol
Normal file
92
contracts/staking/SimpleStake.sol
Normal file
@ -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];
|
||||||
|
}
|
||||||
|
}
|
29
deploy/12_deploy_simple_stake.ts
Normal file
29
deploy/12_deploy_simple_stake.ts
Normal file
@ -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;
|
101
test/testSimpleStake.ts
Normal file
101
test/testSimpleStake.ts
Normal file
@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
x
Reference in New Issue
Block a user