Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 125 additions & 43 deletions src/stake/LayerEdgeStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ contract LayerEdgeStaking is
uint256 public constant SECONDS_IN_YEAR = 365 days;
uint256 public constant PRECISION = 1e18;
uint256 public constant UNSTAKE_WINDOW = 7 days;
// $100 worth of EDGEN (~3k tokens)
uint256 public constant MAX_USERS = 100_000_000;

// Tier percentages
Expand Down Expand Up @@ -91,7 +90,6 @@ contract LayerEdgeStaking is
mapping(address => UserInfo) public users;
mapping(uint256 => address) public stakerAddress;
mapping(address => TierEvent[]) public stakerTierHistory;
mapping(address => uint256) public totalStakersSnapshot;
uint256 public stakerCountInTree;
uint256 public stakerCountOutOfTree;
uint256 public totalStaked;
Expand Down Expand Up @@ -519,14 +517,6 @@ contract LayerEdgeStaking is
);
}

/**
* @notice Get the amount of reward tokens available in the contract
* @return Available rewards
*/
function getAvailableRewards() external view returns (uint256) {
return rewardsReserve;
}

/**
* @notice Get the APY rate for a specific tier during a time period
* @param tier The tier to get the APY for
Expand Down Expand Up @@ -621,6 +611,61 @@ contract LayerEdgeStaking is
return stakerCountInTree + stakerCountOutOfTree;
}

function getCumulativeFrequency(uint256 rank) external view returns (uint256) {
return stakerTree.findByCumulativeFrequency(rank);
}

/*//////////////////////////////////////////////////////////////
UI DATA PROVIDERS
//////////////////////////////////////////////////////////////*/
function getAllInfoOfUser(address userAddr)
external
view
returns (
UserInfo memory user,
Tier tier,
uint256 apy,
uint256 depositTime,
uint256 pendingRewards,
TierEvent[] memory tierHistory
)
{
user = users[userAddr];
tier = getCurrentTier(userAddr);
apy = getUserAPY(userAddr);
depositTime = user.depositTime;
pendingRewards = calculateUnclaimedInterest(userAddr);
tierHistory = stakerTierHistory[userAddr];
}

function getAllStakingInfo()
external
view
returns (
uint256 _totalStaked,
uint256 _stakerCountInTree,
uint256 _stakerCountOutOfTree,
uint256 _rewardsReserve,
uint256 _minStakeAmount,
uint256 _tier1Count,
uint256 _tier2Count,
uint256 _tier3Count
)
{
(_tier1Count, _tier2Count, _tier3Count) = getTierCountForStakerCount(stakerCountInTree);

return (
totalStaked,
stakerCountInTree,
stakerCountOutOfTree,
rewardsReserve,
minStakeAmount,
_tier1Count,
_tier2Count,
_tier3Count
);
}

/*//////////////////////////////////////////////////////////////
INTERNAL FUNCTIONS
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -663,13 +708,7 @@ contract LayerEdgeStaking is
user.isFirstDepositMoreThanMinStake = true;

_recordTierChange(userAddr, tier);

// Record any boundary crossings only if active staker count has changed
if (totalStakersSnapshot[userAddr] != stakerCountInTree) {
_checkBoundariesAndRecord(false);
}

totalStakersSnapshot[userAddr] = stakerCountInTree;
_checkBoundariesAndRecord(false);
}

// Update user balances
Expand Down Expand Up @@ -708,13 +747,7 @@ contract LayerEdgeStaking is
stakerCountInTree--;
user.outOfTree = true;
stakerCountOutOfTree++;

// Record any boundary crossings only if active staker count has changed
if (totalStakersSnapshot[userAddr] != stakerCountInTree) {
_checkBoundariesAndRecord(true);
}

totalStakersSnapshot[userAddr] = stakerCountInTree;
_checkBoundariesAndRecord(true);
}

// Transfer tokens from contract to user
Expand Down Expand Up @@ -770,7 +803,11 @@ contract LayerEdgeStaking is

function _recordTierChange(address user, Tier newTier) internal {
// Get current tier
Tier old = getCurrentTier(user);
Tier old = Tier.Tier3;

if (stakerTierHistory[user].length > 0) {
old = stakerTierHistory[user][stakerTierHistory[user].length - 1].to;
}

// If this is the same tier as before, no change to record
if (
Expand All @@ -796,29 +833,74 @@ contract LayerEdgeStaking is
// new thresholds
(uint256 new_t1, uint256 new_t2,) = getTierCountForStakerCount(n);

// for each boundary, if it shifted by ±1, find the user crossing
if (new_t1 != 0 && new_t1 != old_t1) {
// someone moved across Tier1↔Tier2
// the user at rank = min(old_t1, new_t1)+1 if promotion, or old_t1 if demotion
uint256 crossRank = new_t1 > old_t1
? new_t1 // promotion: the one newly entering Tier1
: old_t1; // demotion: the one kicked out of Tier1
uint256 joinIdCross = stakerTree.findByCumulativeFrequency(crossRank);
address userCross = stakerAddress[joinIdCross];
Tier toTier = _computeTierByRank(joinIdCross, n);
_recordTierChange(userCross, toTier);
// Tier 1 boundary handling
if (new_t1 != 0) {
if (new_t1 != old_t1) {
// Tier boundary change - handle as before
uint256 crossRank = new_t1 > old_t1
? new_t1 // promotion: the one newly entering Tier1
: old_t1; // demotion: the one kicked out of Tier1
_findAndRecordTierChange(crossRank, n);
}
// Handle case where Tier 1 count stays the same
else if (isRemoval && new_t1 > 0) {
// If a user was removed but tier 1 count didn't change
// We need to update the user at position new_t1 (someone from Tier 2 may need promotion)
_findAndRecordTierChange(new_t1, n);
} else if (!isRemoval) {
// If a user was added, the user at position old_t1 might have changed tiers
_findAndRecordTierChange(old_t1, n);
}
}

if (new_t2 != old_t2) {
// Tier2↔Tier3 boundary
uint256 crossRank = new_t2 > old_t2 ? new_t2 : old_t2;
uint256 joinIdCross = stakerTree.findByCumulativeFrequency(crossRank);
address userCross = stakerAddress[joinIdCross];
Tier toTier = _computeTierByRank(joinIdCross, n);
_recordTierChange(userCross, toTier);
// Tier 2 boundary handling
if (new_t1 + new_t2 > 0) {
// Ensure there are stakers in Tier 1 or Tier 2
if (new_t2 != old_t2) {
// Tier 2 boundary changed - handle as before
uint256 crossRank;
if (new_t2 > old_t2) {
// Promotion
crossRank = new_t1 + new_t2; // Add new_t1 count to land on correct index/rank
} else {
// Demotion
crossRank = new_t1 + old_t2; // Add new_t1 count to land on correct index/rank
}
_findAndRecordTierChange(crossRank, n);
}
// Handle case where Tier 2 count stays the same
else if (isRemoval) {
// If a user was removed but tier 2 count didn't change
// We need to update the user at boundary between Tier 2 and Tier 3
uint256 crossRank = new_t1 + new_t2; // Boundary position
_findAndRecordTierChange(crossRank, n);
} else if (!isRemoval) {
// If a user was added, the user at old boundary between Tier 2 and Tier 3 might have changed
uint256 crossRank = old_t1 + old_t2; // Old boundary position
_findAndRecordTierChange(crossRank, n);
}
}
}

/**
* @notice Find the user at a given rank and record the tier change
* @param rank The rank of the user
* @param _stakerCountInTree The total number of stakers
*/
function _findAndRecordTierChange(uint256 rank, uint256 _stakerCountInTree) internal {
uint256 joinIdCross = stakerTree.findByCumulativeFrequency(rank);
address userCross = stakerAddress[joinIdCross];
uint256 _rank = stakerTree.query(joinIdCross);
Tier toTier = _computeTierByRank(_rank, _stakerCountInTree);
_recordTierChange(userCross, toTier);
}

/**
* @notice Compute the tier for a user based on their rank and total stakers
* @param rank The rank of the user
* @param totalStakers The total number of stakers
* @return The tier of the user
*/
function _computeTierByRank(uint256 rank, uint256 totalStakers) internal pure returns (Tier) {
if (rank == 0 || rank > totalStakers) return Tier.Tier3;
(uint256 tier1Count, uint256 tier2Count,) = getTierCountForStakerCount(totalStakers);
Expand Down
Loading