Skip to content

Commit

Permalink
Dev (#221)
Browse files Browse the repository at this point in the history
* amoy settings

* try execute in lib

* tryExecute

* refactor

* amoy settings (#198)

* amoy settings (#198) (#200)

* Network/optimism (#202)

* env parameters

* removed duplicated network

* Dev (#203) (#204)

* amoy settings (#198)

* Network/optimism (#202)

* env parameters

* removed duplicated network

* gas amount (#205)

* 20k gas (#207)

* revert just in case (#208)

* gas 30k (#209)

* Networks/base sepolia (#212)

* initial settings

* verify parameters and token addresses

* final properties

* Fixed quorum fishing attack (#210)

* fixed solidity todo tests

* regression test

* naming

* fix tests coverage (#214)

---------

Co-authored-by: Dmitry Redkin <[email protected]>
Co-authored-by: todesstille <[email protected]>

* Feature/token preallocation (#213)

* allocation added

* refactored

* tests

* environment proxy and tests

* govpool preallocation

* fixes

* added allocator and payee roles

* metadata

* view functions

* added auxiliary contracts deploy

* dependand

* tx.origin

* unlock in multiplier

* function annotations

* fixes

* fixes

* Feature/token factory (#215)

* predeploy token

* handle invalid deploy

* integrated with allocator

* token allocator integration

* Feature/gov tokens (#216)

* burnable pausable

* capped

* tokens

* removed old tests

* fixes (#217)

* revards 1/100

* .env.example

* Feature/staking (#220)

* base staking

* getUserInfo

* removed some data

* refactor

* coverage 100

* event on reclaim

* view finction

* fix active proposals

* deploy staking

* canMove

* unsaved

* events

---------

Co-authored-by: Artem Chystiakov <[email protected]>
  • Loading branch information
todesstille and Arvolear authored Dec 13, 2024
1 parent 39568a2 commit f09a3ba
Show file tree
Hide file tree
Showing 14 changed files with 1,019 additions and 3 deletions.
1 change: 1 addition & 0 deletions contracts/factory/PoolRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ contract PoolRegistry is IPoolRegistry, MultiOwnablePoolContractsRegistry {
string public constant USER_KEEPER_NAME = "USER_KEEPER";
string public constant DISTRIBUTION_PROPOSAL_NAME = "DISTRIBUTION_PROPOSAL";
string public constant TOKEN_SALE_PROPOSAL_NAME = "TOKEN_SALE_PROPOSAL";
string public constant STAKING_PROPOSAL_NAME = "STAKING_PROPOSAL";

string public constant EXPERT_NFT_NAME = "EXPERT_NFT";
string public constant NFT_MULTIPLIER_NAME = "NFT_MULTIPLIER";
Expand Down
4 changes: 4 additions & 0 deletions contracts/gov/GovPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,10 @@ contract GovPool is
return (_nftMultiplier, address(_expertNft), address(_dexeExpertNft), address(_babt));
}

function getPoolRegistryContract() external view override returns (address) {
return _poolRegistry;
}

function getProposals(
uint256 offset,
uint256 limit
Expand Down
306 changes: 306 additions & 0 deletions contracts/gov/proposals/StakingProposal.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "./helpers/AbstractValueDistributor.sol";

import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import "../../interfaces/gov/proposals/IStakingProposal.sol";
import "../../core/Globals.sol";

contract StakingProposal is IStakingProposal, Initializable, AbstractValueDistributor {
using EnumerableSet for *;
using SafeERC20 for IERC20;

uint256 constant MAX_TIERS_AMOUNT = 10;

address public govPoolAddress;
address public userKeeperAddress;

mapping(uint256 => StakingInfo) internal stakingInfos;
mapping(address => EnumerableSet.UintSet) internal _userClaimableTiers;
EnumerableSet.UintSet internal _activeTiers;

uint256 public stakingsCount;

event StakingCreated(
uint256 id,
address rewardToken,
uint256 totalRewardsAmount,
uint256 startedAt,
uint256 deadline,
string metadata
);
event StakingRejected(
address rewardToken,
uint256 totalRewardsAmount,
uint256 startedAt,
uint256 deadline,
string metadata
);
event StakeAdded(uint256 id, address user, uint256 amount);
event RewardClaimed(uint256 id, address user, address rewardToken, uint256 rewardsAmount);

modifier onlyGov() {
require(msg.sender == govPoolAddress, "SP: not a Gov contract");
_;
}

modifier onlyKeeper() {
require(msg.sender == userKeeperAddress, "SP: not a Keeper contract");
_;
}

function __StakingProposal_init(address _govPoolAddress) external initializer {
require(_govPoolAddress != address(0), "SP: Gov is zero");

govPoolAddress = _govPoolAddress;
userKeeperAddress = msg.sender;
}

function createStaking(
address rewardToken,
uint256 rewardAmount,
uint256 startedAt,
uint256 deadline,
string calldata metadata
) external onlyGov {
require(
startedAt < deadline && rewardToken != address(0) && rewardAmount > 0,
"SP: Invalid settings"
);
require(
_recalculateActiveTiers(_activeTiers).length() < MAX_TIERS_AMOUNT,
"SP: Max tiers reached"
);

if (deadline < block.timestamp) {
IERC20(rewardToken).safeTransferFrom(govPoolAddress, address(this), rewardAmount);
IERC20(rewardToken).safeTransfer(govPoolAddress, rewardAmount);
emit StakingRejected(rewardToken, rewardAmount, startedAt, deadline, metadata);
return;
}

uint256 id = ++stakingsCount;
StakingInfo storage info = stakingInfos[id];

info.rewardToken = rewardToken;
info.totalRewardsAmount = rewardAmount;
info.startedAt = startedAt;
info.deadline = deadline;
info.metadata = metadata;

_activeTiers.add(id);

_updatedAt[id] = startedAt;

IERC20(rewardToken).safeTransferFrom(govPoolAddress, address(this), rewardAmount);

emit StakingCreated(id, rewardToken, rewardAmount, startedAt, deadline, metadata);
}

function stake(address user, uint256 amount, uint256 id) external onlyKeeper {
require(isActiveTier(id), "SP: Not Active");

_userClaimableTiers[user].add(id);

_addShares(id, user, amount);

emit StakeAdded(id, user, amount);
}

function claim(uint256 id) external {
_couldClaim(id);
_claim(id);
}

function claimAll() external {
EnumerableSet.UintSet storage claimableTiersList = _userClaimableTiers[msg.sender];
uint256 length = claimableTiersList.length();
for (uint i = length; i > 0; i--) {
uint256 id = claimableTiersList.at(i - 1);
uint256 deadline = stakingInfos[id].deadline;
if (block.timestamp <= deadline) continue;
_claim(id);
}
}

function reclaim(uint256 id) external {
_couldClaim(id);
_reclaim(id);
}

function getTotalStakes(address user) external view returns (uint256 totalStakes) {
uint256[] memory userActiveTiers = _activeTiers.length() <
_userClaimableTiers[user].length()
? _activeTiers.values()
: _userClaimableTiers[user].values();

uint256 length = userActiveTiers.length;
for (uint256 i = 0; i < length; i++) {
uint256 id = userActiveTiers[i];
if (isActiveTier(id)) {
totalStakes += _userDistributions[id][user].shares;
}
}
}

function isActiveTier(uint256 id) public view returns (bool) {
uint256 startedAt = stakingInfos[id].startedAt;
uint256 deadline = stakingInfos[id].deadline;
if (deadline == 0) return false;
return deadline >= block.timestamp && block.timestamp >= startedAt;
}

function getOwedValue(uint256 id, address user_) public view returns (uint256) {
UserDistribution storage userDist = _userDistributions[id][user_];

uint256 startedAt = stakingInfos[id].startedAt;
uint256 deadline = stakingInfos[id].deadline;

if (deadline == 0 || block.timestamp < startedAt) return 0;

uint256 time = block.timestamp > deadline ? deadline : block.timestamp;

return
(userDist.shares * (_getFutureCumulativeSum(id, time) - userDist.cumulativeSum)) /
PRECISION +
userDist.owedValue;
}

function getUserInfo(
address user
) external view returns (TierUserInfo[] memory tiersUserInfo) {
EnumerableSet.UintSet storage claimableTiers = _userClaimableTiers[user];

uint256 length = claimableTiers.length();
tiersUserInfo = new TierUserInfo[](length);

for (uint256 i = 0; i < length; i++) {
uint256 id = claimableTiers.at(i);

StakingInfo storage info = stakingInfos[id];

tiersUserInfo[i].tierId = id;
tiersUserInfo[i].isActive = isActiveTier(id);
tiersUserInfo[i].rewardToken = info.rewardToken;
tiersUserInfo[i].startedAt = info.startedAt;
tiersUserInfo[i].deadline = info.deadline;
tiersUserInfo[i].currentStake = _userDistributions[id][user].shares;
tiersUserInfo[i].currentRewards = getOwedValue(id, user);
tiersUserInfo[i].tierCurrentStakes = _totalShares[id];
}
}

function getStakingInfo(
uint256[] memory ids
) public view returns (StakingInfoView[] memory stakingInfo) {
stakingInfo = new StakingInfoView[](ids.length);
for (uint i = 0; i < ids.length; i++) {
StakingInfoView memory info = stakingInfo[i];

uint256 id = ids[i];
StakingInfo storage tierInfo = stakingInfos[id];

info.id = id;
info.metadata = tierInfo.metadata;
info.rewardToken = tierInfo.rewardToken;
info.totalRewardsAmount = tierInfo.totalRewardsAmount;
info.startedAt = tierInfo.startedAt;
info.deadline = tierInfo.deadline;
info.isActive = info.startedAt <= block.timestamp && block.timestamp <= info.deadline;
info.totalStaked = _totalShares[id];
info.owedToProtocol = _owedToProtocol[id];
}
}

function getActiveStakings() external view returns (StakingInfoView[] memory stakingInfo) {
uint256[] memory claimableIds = _activeTiers.values();
uint256 length;
for (uint256 i = 0; i < claimableIds.length; i++) {
if (isActiveTier(claimableIds[i])) {
length++;
}
}

uint256[] memory ids = new uint256[](length);

uint256 counter;
for (uint256 i = 0; i < claimableIds.length; i++) {
uint256 id = claimableIds[i];
if (isActiveTier(id)) {
ids[counter] = id;
counter++;
}
}

stakingInfo = getStakingInfo(ids);
}

function _recalculateActiveTiers(
EnumerableSet.UintSet storage _tiers
) internal returns (EnumerableSet.UintSet storage) {
uint256 length = _tiers.length();
for (uint256 i = length; i > 0; i--) {
uint256 id = _tiers.at(i - 1);
StakingInfo storage info = stakingInfos[id];

if (info.deadline < block.timestamp) {
_tiers.remove(id);
}
}
return _tiers;
}

function _couldClaim(uint256 id) internal view {
StakingInfo storage info = stakingInfos[id];
uint256 deadline = info.deadline;
require(deadline != 0, "SP: invalid id");
require(deadline < block.timestamp, "SP: Still active");
}

function _claim(uint256 id) internal {
StakingInfo storage info = stakingInfos[id];
uint256 deadline = info.deadline;

address rewardToken = info.rewardToken;
address user = msg.sender;

_userClaimableTiers[user].remove(id);

_updateOnTime(id, user, deadline);
uint256 amountToPay = _userDistributions[id][user].owedValue;
if (amountToPay != 0) {
_userDistributions[id][user].owedValue = 0;
IERC20(rewardToken).safeTransfer(user, amountToPay);
emit RewardClaimed(id, user, rewardToken, amountToPay);
}
}

function _reclaim(uint256 id) internal {
StakingInfo storage info = stakingInfos[id];
uint256 deadline = info.deadline;
address rewardToken = info.rewardToken;

_updateFromProtocol(id, deadline);
uint256 amountToPay = _owedToProtocol[id];
if (amountToPay != 0) {
_owedToProtocol[id] = 0;
IERC20(rewardToken).safeTransfer(govPoolAddress, amountToPay);
emit RewardClaimed(id, govPoolAddress, rewardToken, amountToPay);
}
}

function _getValueToDistribute(
uint256 id,
uint256 timeUpTo_,
uint256 timeLastUpdate_
) internal view virtual override returns (uint256) {
StakingInfo storage info = stakingInfos[id];
return
(info.totalRewardsAmount * (timeUpTo_ - timeLastUpdate_)) /
(info.deadline - info.startedAt);
}
}
Loading

0 comments on commit f09a3ba

Please sign in to comment.