diff --git a/contracts/dividend.sol b/contracts/dividend.sol index 24a449b..e0b8c31 100644 --- a/contracts/dividend.sol +++ b/contracts/dividend.sol @@ -5,7 +5,14 @@ import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; contract DividendContract is ReentrancyGuard { address public stakingToken; - uint256 public cycleLength; + + // the max length of the cycle in blocks + uint256 public cycleMaxLength; + + // current cycle index, start at 0 + uint256 public currentCycleIndex; + + // the start block of the cycle of the contract uint256 public cycleStartBlock; struct RewardInfo { @@ -14,42 +21,50 @@ contract DividendContract is ReentrancyGuard { } struct CycleInfo { - // the stake info of the cycle + // The start block of the cycle + uint256 startBlock; + + // the total stake amount of the curent cycle uint256 totalStaked; - mapping(address => uint256) staked; - address[] stakers; // the reward info of the cycle RewardInfo[] rewards; - - // the reward settle info of the cycle for each user - mapping(address => bool) settled; - - // the cycle is settled or not - bool cycleSettled; } + // the total staked amount of the contract of all users + uint256 public totalStaked; + // the cycle info of the contract CycleInfo[] public cycles; - // the dividend info of the user that already settled but not withdraw yet - mapping(address => RewardInfo[]) public dividends; + // the staking record of the user + struct StakeRecord { + uint256 cycleIndex; + uint256 amount; + uint256 newAmount; + } + mapping(address => StakeRecord[]) UserStakeRecords; + + // the dividend state of the user + mapping(bytes32 => bool) public withdrawDividendState; event Deposit(uint256 amount, address token); event Stake(address indexed user, uint256 amount); + event Unstake(address indexed user, uint256 amount); + event NewCycle(uint256 cycleIndex, uint256 startBlock); - constructor(address _stakingToken, uint256 _cycleLength) { + constructor(address _stakingToken, uint256 _cycleMaxLength) { stakingToken = _stakingToken; - cycleLength = _cycleLength; + cycleMaxLength = _cycleMaxLength; cycleStartBlock = block.number; } function getCurrentCycleIndex() public view returns (uint256) { - return (block.number - cycleStartBlock) / cycleLength; + return currentCycleIndex; } - function getCurrentCycle() internal returns (CycleInfo storage) { + function getCurrentCycle() public view returns (CycleInfo storage) { uint256 currentCycleIndex = getCurrentCycleIndex(); if (cycles.length <= currentCycleIndex) { cycles.push(); @@ -57,15 +72,6 @@ contract DividendContract is ReentrancyGuard { return cycles[currentCycleIndex]; } - function getNextCycle() internal returns (CycleInfo storage) { - uint256 currentCycleIndex = getCurrentCycleIndex(); - if (cycles.length <= currentCycleIndex + 1) { - cycles.push(); - } - - return cycles[currentCycleIndex + 1]; - } - // deposit token to the current cycle function _depositToken(address token, uint256 amount) internal { require(amount > 0, "Cannot deposit 0"); @@ -85,10 +91,14 @@ contract DividendContract is ReentrancyGuard { receive() external payable { + _tryNewCycle(); + _depositToken(address(0), msg.value); } function deposit(uint256 amount, address token) external nonReentrant { + _tryNewCycle(); + require(token != address(stakingToken), "Cannot deposit Staking token"); require(token != address(0), "Use native transfer to deposit ETH"); @@ -97,223 +107,167 @@ contract DividendContract is ReentrancyGuard { _depositToken(token, amount); } - function updateTokenBalance(address token) external nonReentrant { - require(token != address(stakingToken), "Cannot update Staking token"); - - uint256 balance; - if (token == address(0)) { - // If the token address is 0, return the ETH balance of the contract - balance = address(this).balance; - } else { - // If the token address is not 0, return the ERC20 token balance of the contract - balance = IERC20(token).balanceOf(address(this)); - } + function getStakedAmount(address user, uint256 cycleIndex) public view returns (uint256) { + StakeRecord[] memory stakeRecords = UserStakeRecords[user]; + for (uint i = stakeRecords.length - 1; i != uint(-1); i--) { - // find the token in the rewards array and update the amount - RewardInfo[] storage rewards = getCurrentCycle().rewards; - for (uint256 i = 0; i < rewards.length; i++) { - if (rewards[i].token == token) { - rewards[i].amount = balance; - return; + // stakeRecords里面的对应周期的质押数据,都是对应周期发起的操作导致的状态,所以需要进入下一个周期才会生效,所以这里使用 < 而不是 <= + if (stakeRecords[i].cycleIndex < cycleIndex) { + return stakeRecords[i].amount; } } - // if the token is not found in the rewards array, add it - rewards.push(RewardInfo(token, balance)); + return 0; } - // stake tokens to next cycle function stake(uint256 amount) external nonReentrant { require(amount > 0, "Cannot stake 0"); require(IERC20(stakingToken).transferFrom(msg.sender, address(this), amount), "Stake failed"); - CycleInfo storage cycle = getNextCycle(); - - cycle.totalStaked += amount; + uint256 currentCycleIndex = getCurrentCycleIndex(); - // update the stakers array if the user is not already in it - if (cycle.staked[msg.sender] == 0) { - cycle.stakers.push(msg.sender); + StakeRecord[] storage stakeRecords = UserStakeRecords[msg.sender]; + if (stakeRecords.length == 0) { + stakeRecords.push(StakeRecord(currentCycleIndex, amount, amount)); + } else { + StackRecord storage lastStakeRecord = stakeRecords[stakeRecords.length - 1]; + if (lastStakeRecord.cycleIndex == currentCycleIndex) { + lastStakeRecord.amount += amount; + lastStakeRecord.newAmount += amount; + } else { + stakeRecords.push(StakeRecord(currentCycleIndex, lastStakeRecord.amount + amount, amount)); + } } - cycle.staked[msg.sender] += amount; + // update the total staked amount of the contract + totalStaked += amount; + // emit the stake event emit Stake(msg.sender, amount); } // withdraw staking tokens from current first and then next cycles - // withdraw amount must be less than or equal to the staked amount in both cycles - function withdraw(uint256 amount) external nonReentrant { - // TODO check point to settle cycles that need to be settled - settleCycle(); + // withdraw amount must be less than or equal to the staked amount + function unstake(uint256 amount) external nonReentrant { + StakeRecord[] storage stakeRecords = UserStakeRecords[msg.sender]; + require(stakeRecords.length > 0, "No stake record found"); + + // get the last stake record of the user + StackRecord storage lastStakeRecord = stakeRecords[stakeRecords.length - 1]; + require(lastStakeRecord.amount >= amount, "Insufficient staked amount"); + + // 如果存在当前周期的质押操作,那么这个质押操作是可以直接撤销的不影响周期数据(当前质押要在下个周期进入cycleInfo中) + // 如果不是当前周期的质押操作,或者当前周期的质押数量不足,那么这个质押操作是需要从上个周期关联的cycleInfo数据中减去的 + uint256 currentCycleIndex = getCurrentCycleIndex(); + if (lastStakeRecord.cycleIndex == currentCycleIndex) { + if (lastStakeRecord.addAmount >= amount) { + lastStakeRecord.amount -= amount; + lastStakeRecord.addAmount -= amount; + } else { + uint256 diff = amount - lastStakeRecord.addAmount; - CycleInfo storage currentCycle = getCurrentCycle(); - CycleInfo storage nextCycle = getNextCycle(); + StakeRecord memory prevStakeRecord = stakeRecords[stakeRecords.length - 2]; + + // the last record is unstaked all and is empty, delete it + delete stakeRecords[stakeRecords.length - 1]; - uint256 total = currentCycle.staked[msg.sender] + nextCycle.staked[msg.sender]; - require(amount <= total, "Insufficient staked amount"); + // the prev record all unstaked with the diff amount + prevStakeRecord.amount -= diff; - if (amount <= currentCycle.staked[msg.sender]) { - currentCycle.totalStaked -= amount; - currentCycle.staked[msg.sender] -= amount; + // update the total staked amount of the corresponding cycle total staked amount + cycles[prevStakeRecord.cycleIndex].totalStaked -= diff; + } } else { - uint256 currentCycleStaked = currentCycle.staked[msg.sender]; - currentCycle.totalStaked -= currentCycleStaked; - currentCycle.staked[msg.sender] = 0; + lastStakeRecord.amount -= amount; - nextCycle.totalStaked -= (amount - currentCycleStaked); - nextCycle.staked[msg.sender] -= (amount - currentCycleStaked); + cycles[lastStakeRecord.cycleIndex].totalStaked -= amount; } + + require(IERC20(stakingToken).transfer(msg.sender, amount), "Unstake failed"); - require(IERC20(stakingToken).transfer(msg.sender, amount), "Withdraw failed"); + emit Unstake(msg.sender, amount); } + // check point for the new cycle + function _tryNewCycle() internal { + uint256 currentBlock = block.number; + + CycleInfo storage currentCycle = getCurrentCycle(); + if (currentBlock - currentCycle.startBlock >= cycleMaxLength) { + currentCycleIndex = currentCycleIndex + 1; - /** - * @dev Claim rewards for the current cycle - * 结算前一个周期以及之前的所有周期(如果周期尚未结算的话),处理如下逻辑 - * 1. 当前周期N - * 2. 向前查找到第一个尚未结算的周期M,M-N>=1 - * 3. 对周期M进行结算:质押池不为空的话,把质押池直接赋值给周期M+1;如果质押池为空,那么直接把分红池赋值给周期M(分红池不为空的话) - * 4. 计算完毕后,对该周期设置标志位settled=true表示已经结算完毕,不可重复结算 - */ - function settleCycle() public { - uint256 currentCycleIndex = getCurrentCycleIndex(); + CycleInfo storage newCycle = cycles[currentCycleIndex]; + newCycle.startBlock = currentBlock; + newCycle.totalStaked = totalStaked; + + if (currentCycle.totalStaked == 0) { + newCycle.rewards = currentCycle.rewards; + } - // if the current cycle is the first cycle, return directly - if (currentCycleIndex == 0) { - return; + emit NewCycle(currentCycleIndex, currentBlock); } + } - // if the previous cycle is already settled, return directly - uint256 prevCycleIndex = currentCycleIndex - 1; - if (cycles[prevCycleIndex].cycleSettled) { - return; - } + // check if the user has settled the rewards for the cycle + function isDividendWithdrawed(address user, uint256 cycleIndex, uint256 token) public view returns (bool) { + bytes32 key = keccak256(abi.encodePacked(user, cycleIndex, token)); + return withdrawDividendState[key]; + } - // find the first unsettled cycle from front to back - while (prevCycleIndex > 0 && !cycles[prevCycleIndex].cycleSettled) { - prevCycleIndex--; - } - if (cycles[prevCycleIndex].cycleSettled) { - prevCycleIndex++; - } + // claim rewards for the cycle + function withdrawDevidends(uint256[] cycleIndexs, uint256[] tokens) external nonReentrant { + require(cycleIndexs.length > 0, "No cycle index"); + require(tokens.length > 0, "No token"); - require(prevCycleIndex < currentCycleIndex, "No unsettled cycle found"); + RewardInfo[] storage rewards = []; - // settle the cycle from prevCycleIndex to currentCycleIndex - for (uint256 i = prevCycleIndex; i < currentCycleIndex; i++) { - _settleCycle(i); - } - } + for (uint i = 0; i < cycleIndexs.length; i++) { + uint256 cycleIndex = cycleIndexs[i]; + require(cycleIndex < currentCycleIndex, "Cannot claim current cycle"); - function _settleCycle(uint index) internal { - CycleInfo storage lastSettledCycle = cycles[index]; - CycleInfo storage currentCycle = cycles[index + 1]; + // withdraw every token + for (uint j = 0; j < tokens.length; j++) { + uint256 token = tokens[j]; + require(!isDividendWithdrawed(msg.sender, cycleIndex, token), "Already claimed"); - require(lastSettledCycle.cycleSettled == false, "Cycle already settled"); - require(currentCycle.cycleSettled == false, "Cycle already settled"); - require(index < getCurrentCycleIndex(), "Cannot claim current cycle"); + CycleInfo storage cycle = cycles[cycleIndex]; - // if the last settled cycle has staked amount, transfer the staked amount to the current cycle - if (lastSettledCycle.totalStaked > 0) { - if (currentCycle.totalStaked == 0) { - currentCycle.totalStaked = lastSettledCycle.totalStaked; - currentCycle.stakers = lastSettledCycle.stakers; - for (uint256 i = 0; i < lastSettledCycle.stakers.length; i++) { - address staker = lastSettledCycle.stakers[i]; - currentCycle.staked[staker] = lastSettledCycle.staked[staker]; + uint256 totalStaked = cycle.totalStaked; + if (totalStaked == 0) { + continue; } - } else { - // merge the staked amount of the last settled cycle to the current cycle - currentCycle.totalStaked += lastSettledCycle.totalStaked; - for (uint256 i = 0; i < lastSettledCycle.stakers.length; i++) { - address staker = lastSettledCycle.stakers[i]; - if (currentCycle.staked[staker] == 0) { - currentCycle.stakers.push(staker); - } - currentCycle.staked[staker] += lastSettledCycle.staked[staker]; - } - } - - } else { - // if the last settled cycle has rewards and no staked token, then transfer the rewards to the current cycle - if (lastSettledCycle.rewards.length > 0) { - currentCycle.rewards = lastSettledCycle.rewards; - } - } - lastSettledCycle.cycleSettled = true; - } + uint256 userStaked = getStakedAmount(msg.sender, cycleIndex); - // check if the user has settled the rewards for the cycle - function isDividendSettled(address user, uint256 cycleIndex) public view returns (bool) { - return cycles[cycleIndex].settled[user]; - } + // find the token reward of the cycle + uint256 rewardAmount = 0; + for (uint k = 0; k < cycle.rewards.length; k++) { + RewardInfo storage reward = cycle.rewards[k]; + if (reward.token == token) { + rewardAmount = reward.amount * userStaked / cycle.totalStaked; + break; + } + } - // claim rewards for the cycle - function settleDevidend(uint256 cycleIndex) external nonReentrant { - // TODO check point to settle cycles that need to be settled - settleCycle(); - - require(cycleIndex < getCurrentCycleIndex(), "Cannot claim current cycle"); - - CycleInfo storage cycle = cycles[cycleIndex]; - require(!cycle.settled[msg.sender], "Already claimed"); - - uint256 totalStaked = cycle.totalStaked; - uint256 userStaked = cycle.staked[msg.sender]; - require(userStaked > 0, "No staked amount"); - require(cycle.rewards.length > 0, "No reward amount"); - - cycle.settled[msg.sender] = true; - - // calculate the each reward amount for the user, and then send the reward to the user - for (uint256 i = 0; i < cycle.rewards.length; i++) { - RewardInfo storage reward = cycle.rewards[i]; - uint256 rewardAmount = reward.amount * userStaked / totalStaked; - if (rewardAmount > 0) { - _addDividend(msg.sender, reward.token, rewardAmount); - } - } - } + if (rewardAmount > 0) { + rewards.push(RewardInfo(token, rewardAmount)); + } - // add dividend to user when settle the rewards - function _addDividend(address user, address token, uint256 amount) internal { - RewardInfo[] storage userDividends = dividends[user]; - for (uint i = 0; i < userDividends.length; i++) { - if (userDividends[i].token == token) { - userDividends[i].amount += amount; - return; + // set the withdraw state of the user and the cycle and the token + bytes32 key = keccak256(abi.encodePacked(msg.sender, cycleIndex, token)); + withdrawDividendState[key] = true; } } - dividends[user].push(RewardInfo(token, amount)); - } - - // get all dividends for the user than settled but not withdraw yet - function getDividend() public view returns (RewardInfo[] memory) { - return dividends[msg.sender]; - } - - // withdraw all rewards for the user - function withdrawDividend() external nonReentrant { - address user = msg.sender; - - RewardInfo[] storage userDividends = dividends[user]; - for (uint i = 0; i < userDividends.length; i++) { - uint256 dividend = userDividends[i].amount; - if (dividend > 0) { - // userDividends[i].amount = 0; - if (userDividends[i].token == address(0)) { // 如果token地址为0,表示是ETH - payable(user).transfer(dividend); - } else { - IERC20(userDividends[i].token).transfer(user, dividend); - } + // do the transfer + for (uint i = 0; i < rewards.length; i++) { + RewardInfo storage reward = rewards[i]; + if (reward.token == address(0)) { + payable(msg.sender).transfer(reward.amount); + } else { + IERC20(reward.token).transfer(msg.sender, reward.amount); } } - - delete dividends[user]; // delete all dividends for the user after withdraw all } } \ No newline at end of file