From 4dbfd210e00ba677e101d471ad083d01e443010b Mon Sep 17 00:00:00 2001 From: painiteleo Date: Fri, 17 May 2024 17:22:16 +0800 Subject: [PATCH] feat(ldystaking): implement weight mechanism --- contracts/foundry/test/LDYStaking.t.sol | 115 ++++++++++++++++-------- contracts/foundry/test/LToken.t.sol | 22 ++--- contracts/foundry/test/PreMining.t.sol | 22 ++--- contracts/src/LDYStaking.sol | 80 +++++++++++++---- 4 files changed, 161 insertions(+), 78 deletions(-) diff --git a/contracts/foundry/test/LDYStaking.t.sol b/contracts/foundry/test/LDYStaking.t.sol index 84a65cc3..a92881b9 100644 --- a/contracts/foundry/test/LDYStaking.t.sol +++ b/contracts/foundry/test/LDYStaking.t.sol @@ -17,9 +17,10 @@ contract LDYStakingTest is Test, ModifiersExpectations { LDYStaking ldyStaking; GenericERC20 ldyToken; - uint256[] public stakingDurations; - uint256 public constant oneMonth = 31 * 24 * 60 * 60; - uint256 public initRewardsDuration = 12 * oneMonth; + + uint256 public constant OneMonth = 31 * 24 * 60 * 60; + LDYStaking.StakeDurationInfo[] public stakingDurationInfos; + uint256 public initRewardsDuration = 12 * OneMonth; function setUp() public { // Deploy GlobalOwner @@ -46,13 +47,13 @@ contract LDYStakingTest is Test, ModifiersExpectations { ldyToken = new GenericERC20("Ledgity Token", "LDY", 18); vm.label(address(ldyToken), "LDY token"); - stakingDurations = [ - 1 * oneMonth, - 6 * oneMonth, - 12 * oneMonth, - 24 * oneMonth, - 36 * oneMonth - ]; + stakingDurationInfos.push(LDYStaking.StakeDurationInfo(0 * OneMonth, 10000)); + stakingDurationInfos.push(LDYStaking.StakeDurationInfo(1 * OneMonth, 10000)); + stakingDurationInfos.push(LDYStaking.StakeDurationInfo(6 * OneMonth, 10000)); + stakingDurationInfos.push(LDYStaking.StakeDurationInfo(12 * OneMonth, 10000)); + stakingDurationInfos.push(LDYStaking.StakeDurationInfo(24 * OneMonth, 10000)); + stakingDurationInfos.push(LDYStaking.StakeDurationInfo(36 * OneMonth, 10000)); + // Deploy LDYStaking LDYStaking impl4 = new LDYStaking(); ERC1967Proxy proxy4 = new ERC1967Proxy(address(impl4), ""); @@ -62,8 +63,8 @@ contract LDYStakingTest is Test, ModifiersExpectations { address(globalPause), address(globalBlacklist), address(ldyToken), - stakingDurations, - 12 * oneMonth, + stakingDurationInfos, + 12 * OneMonth, 1000 * 1e18 ); @@ -85,8 +86,8 @@ contract LDYStakingTest is Test, ModifiersExpectations { address(globalPause), address(globalBlacklist), address(ldyToken), - stakingDurations, - 12 * oneMonth, + stakingDurationInfos, + 12 * OneMonth, 1000 * 1e18 ); } @@ -118,8 +119,8 @@ contract LDYStakingTest is Test, ModifiersExpectations { vm.expectRevert("amount = 0"); ldyStaking.stake(0, 0); - vm.expectRevert("invalid staking period"); - ldyStaking.stake(1, uint8(stakingDurations.length)); + vm.expectRevert("Invalid staking period"); + ldyStaking.stake(1, uint8(stakingDurationInfos.length)); vm.expectRevert("ERC20: insufficient allowance"); ldyStaking.stake(1, 0); @@ -135,7 +136,7 @@ contract LDYStakingTest is Test, ModifiersExpectations { vm.assume(account != address(ldyStaking)); vm.assume(amount != 0); - stakingPeriodIndex = uint8(bound(stakingPeriodIndex, 0, stakingDurations.length - 1)); + stakingPeriodIndex = uint8(bound(stakingPeriodIndex, 0, stakingDurationInfos.length - 1)); amount = bound(amount, 1, ldyStaking.stakeAmountForPerks() - 1); // deposit ldy token into the account @@ -189,7 +190,8 @@ contract LDYStakingTest is Test, ModifiersExpectations { vm.assume(account != address(ldyStaking)); vm.assume(amount != 0); - stakingPeriodIndex = uint8(bound(stakingPeriodIndex, 2, stakingDurations.length - 1)); + // stakingPeriodIndex is at least 3(1 year) to satisify perks condition in this case + stakingPeriodIndex = uint8(bound(stakingPeriodIndex, 3, stakingDurationInfos.length - 1)); amount = bound( amount, ldyStaking.stakeAmountForPerks(), @@ -204,7 +206,6 @@ contract LDYStakingTest is Test, ModifiersExpectations { ldyToken.approve(address(ldyStaking), amount); ldyStaking.stake(amount, stakingPeriodIndex); vm.stopPrank(); - assertEq(ldyStaking.tierOf(account), 3); } @@ -214,7 +215,7 @@ contract LDYStakingTest is Test, ModifiersExpectations { uint256 amount, uint8 stakingPeriodIndex ) public { - stakingPeriodIndex = uint8(bound(stakingPeriodIndex, 0, stakingDurations.length - 1)); + stakingPeriodIndex = uint8(bound(stakingPeriodIndex, 0, stakingDurationInfos.length - 1)); vm.assume(account1 != address(0)); vm.assume(account1 != address(ldyToken)); vm.assume(account1 != address(ldyStaking)); @@ -275,7 +276,9 @@ contract LDYStakingTest is Test, ModifiersExpectations { vm.assume(account != address(ldyToken)); vm.assume(amount != 0); amount = bound(amount, 1, 1_000_000 * 10 ** ldyToken.decimals()); - stakingPeriodIndex = uint8(bound(stakingPeriodIndex, 0, stakingDurations.length - 1)); + + // stakingPeriodIndex is at least 1 in this case + stakingPeriodIndex = uint8(bound(stakingPeriodIndex, 1, stakingDurationInfos.length - 1)); deal(address(ldyToken), account, amount); // stake @@ -285,18 +288,21 @@ contract LDYStakingTest is Test, ModifiersExpectations { vm.stopPrank(); // unstake fail - vm.expectRevert("not allowed unstaking in the staking period"); + vm.expectRevert("Cannot unstake during staking period"); vm.prank(account); ldyStaking.unstake(amount, 0); - uint256 skipDuration = stakingDurations[stakingPeriodIndex]; + LDYStaking.StakeDurationInfo memory stakeDurationInfo = stakingDurationInfos[ + stakingPeriodIndex + ]; + uint256 skipDuration = stakeDurationInfo.duration; skip(skipDuration); - vm.expectRevert("insufficient amount"); + vm.expectRevert("Insufficient unstake amount"); vm.prank(account); ldyStaking.unstake(amount + 1, 0); - vm.expectRevert("invalid stakeIndex"); + vm.expectRevert("Invalid stakeIndex"); vm.prank(account); ldyStaking.unstake(amount + 1, 100); } @@ -316,8 +322,8 @@ contract LDYStakingTest is Test, ModifiersExpectations { amount1 = bound(amount1, 1, 1_000_000 * 10 ** ldyToken.decimals()); amount2 = bound(amount1, 1, 1_000_000 * 10 ** ldyToken.decimals()); - stakingPeriodIndex1 = uint8(bound(stakingPeriodIndex1, 0, stakingDurations.length - 1)); - stakingPeriodIndex2 = uint8(bound(stakingPeriodIndex2, 0, stakingDurations.length - 1)); + stakingPeriodIndex1 = uint8(bound(stakingPeriodIndex1, 0, stakingDurationInfos.length - 1)); + stakingPeriodIndex2 = uint8(bound(stakingPeriodIndex2, 0, stakingDurationInfos.length - 1)); deal(address(ldyToken), account1, amount1 + amount2); // account1 stakes ldy into the ldyStaking contract first time @@ -334,7 +340,10 @@ contract LDYStakingTest is Test, ModifiersExpectations { assertEq(earnedArray.length, 2); // account1 unstakes token from staking pool 1 - uint256 skipDuration1 = stakingDurations[stakingPeriodIndex1]; + LDYStaking.StakeDurationInfo memory stakeDurationInfo1 = stakingDurationInfos[ + stakingPeriodIndex1 + ]; + uint256 skipDuration1 = stakeDurationInfo1.duration; skip(skipDuration1); vm.startPrank(account1); uint256 earned1 = ldyStaking.earned(account1, 0); @@ -347,7 +356,11 @@ contract LDYStakingTest is Test, ModifiersExpectations { assertEq(earnedArray.length, 1); // account1 unstakes token from staking pool 2 - uint256 skipDuration2 = stakingDurations[stakingPeriodIndex2]; + LDYStaking.StakeDurationInfo memory stakeDurationInfo2 = stakingDurationInfos[ + stakingPeriodIndex2 + ]; + uint256 skipDuration2 = stakeDurationInfo2.duration; + skip(skipDuration2); vm.startPrank(account1); uint256 earned2 = ldyStaking.earned(account1, 0); @@ -368,7 +381,7 @@ contract LDYStakingTest is Test, ModifiersExpectations { amount = bound(amount, 100, 1_000_000 * 10 ** ldyToken.decimals()); - stakingPeriodIndex = uint8(bound(stakingPeriodIndex, 0, stakingDurations.length - 1)); + stakingPeriodIndex = uint8(bound(stakingPeriodIndex, 0, stakingDurationInfos.length - 1)); deal(address(ldyToken), account, amount); // account stakes ldy into the ldyStaking contract @@ -378,7 +391,10 @@ contract LDYStakingTest is Test, ModifiersExpectations { vm.stopPrank(); // account unstakes part of token - uint256 skipDuration = stakingDurations[stakingPeriodIndex]; + LDYStaking.StakeDurationInfo memory stakeDurationInfo = stakingDurationInfos[ + stakingPeriodIndex + ]; + uint256 skipDuration = stakeDurationInfo.duration; uint256 partAmount = bound(amount, 1, amount - 1); skip(skipDuration); vm.startPrank(account); @@ -417,7 +433,7 @@ contract LDYStakingTest is Test, ModifiersExpectations { amount = bound(amount, 100, 1_000_000 * 10 ** ldyToken.decimals()); - stakingPeriodIndex = uint8(bound(stakingPeriodIndex, 0, stakingDurations.length - 1)); + stakingPeriodIndex = uint8(bound(stakingPeriodIndex, 0, stakingDurationInfos.length - 1)); deal(address(ldyToken), account, amount); // account stakes ldy into the ldyStaking contract @@ -426,7 +442,7 @@ contract LDYStakingTest is Test, ModifiersExpectations { ldyStaking.stake(amount, stakingPeriodIndex); vm.stopPrank(); - vm.expectRevert("invalid stakeIndex"); + vm.expectRevert("Invalid stakeIndex"); vm.prank(account); ldyStaking.getReward(100); } @@ -437,9 +453,13 @@ contract LDYStakingTest is Test, ModifiersExpectations { vm.assume(account != address(ldyStaking)); vm.assume(amount != 0); - amount = bound(amount, 100, 1_000_000 * 10 ** ldyToken.decimals()); + amount = bound( + amount, + 10000 * 10 ** ldyToken.decimals(), + 1_000_000 * 10 ** ldyToken.decimals() + ); - stakingPeriodIndex = uint8(bound(stakingPeriodIndex, 0, stakingDurations.length - 1)); + stakingPeriodIndex = uint8(bound(stakingPeriodIndex, 0, stakingDurationInfos.length - 1)); deal(address(ldyToken), account, amount); // account stakes ldy into the ldyStaking contract @@ -455,10 +475,29 @@ contract LDYStakingTest is Test, ModifiersExpectations { assertEq(rewards0, 0); skip(100); + uint256 earned1 = ldyStaking.earned(account, 0); vm.prank(account); ldyStaking.getReward(0); uint256 rewards1 = ldyToken.balanceOf(account); - assertGt(rewards1, 0); + assertGe(rewards1, earned1); + + // After rewards duration(1year), rewards2 must greater than rewards1 + skip(12 * OneMonth); + uint256 earned2 = ldyStaking.earned(account, 0); + vm.prank(account); + ldyStaking.getReward(0); + uint256 rewards2 = ldyToken.balanceOf(account); + assertGt(earned2, 0); + assertGe(rewards2, earned2); + + // After 1year, rewards3 must be equal to rewards2, as there's no rewards accumulation + skip(12 * OneMonth); + uint256 earned3 = ldyStaking.earned(account, 0); + vm.prank(account); + ldyStaking.getReward(0); + uint256 rewards3 = ldyToken.balanceOf(account); + assertGe(earned3, 0); + assertEq(rewards3, rewards2); } function test_SetRewardsDurationByOwner() public { @@ -467,9 +506,9 @@ contract LDYStakingTest is Test, ModifiersExpectations { vm.prank(nonOwner); ldyStaking.setRewardsDuration(123); - skip(oneMonth); + skip(OneMonth); - uint256 newRewardsDuration = 6 * oneMonth; + uint256 newRewardsDuration = 6 * OneMonth; vm.expectRevert("reward duration is not finished"); ldyStaking.setRewardsDuration(newRewardsDuration); diff --git a/contracts/foundry/test/LToken.t.sol b/contracts/foundry/test/LToken.t.sol index 39ebe881..c7934dce 100644 --- a/contracts/foundry/test/LToken.t.sol +++ b/contracts/foundry/test/LToken.t.sol @@ -147,7 +147,8 @@ contract Tests is Test, ModifiersExpectations { address payable withdrawerWallet = payable(address(bytes20("withdrawerWallet"))); address payable fundWallet = payable(address(bytes20("fundWallet"))); - uint256[] stakingDurations; + uint256 public constant OneMonth = 31 * 24 * 60 * 60; + LDYStaking.StakeDurationInfo[] public stakingDurationInfos; function setUp() public { // Deploy GlobalOwner @@ -175,14 +176,13 @@ contract Tests is Test, ModifiersExpectations { ldyToken = new GenericERC20("Ledgity Token", "LDY", 18); vm.label(address(ldyToken), "LDY token"); - uint256 oneMonth = 31 * 24 * 60 * 60; - stakingDurations = [ - 1 * oneMonth, - 6 * oneMonth, - 12 * oneMonth, - 24 * oneMonth, - 36 * oneMonth - ]; + stakingDurationInfos.push(LDYStaking.StakeDurationInfo(0 * OneMonth, 10000)); + stakingDurationInfos.push(LDYStaking.StakeDurationInfo(1 * OneMonth, 10000)); + stakingDurationInfos.push(LDYStaking.StakeDurationInfo(6 * OneMonth, 10000)); + stakingDurationInfos.push(LDYStaking.StakeDurationInfo(12 * OneMonth, 10000)); + stakingDurationInfos.push(LDYStaking.StakeDurationInfo(24 * OneMonth, 10000)); + stakingDurationInfos.push(LDYStaking.StakeDurationInfo(36 * OneMonth, 10000)); + // Deploy LDYStaking LDYStaking impl4 = new LDYStaking(); ERC1967Proxy proxy4 = new ERC1967Proxy(address(impl4), ""); @@ -192,8 +192,8 @@ contract Tests is Test, ModifiersExpectations { address(globalPause), address(globalBlacklist), address(ldyToken), - stakingDurations, - 12 * oneMonth, + stakingDurationInfos, + 12 * OneMonth, 1000 * 1e18 ); diff --git a/contracts/foundry/test/PreMining.t.sol b/contracts/foundry/test/PreMining.t.sol index d8557d72..e7210abc 100644 --- a/contracts/foundry/test/PreMining.t.sol +++ b/contracts/foundry/test/PreMining.t.sol @@ -43,7 +43,8 @@ contract Tests is Test, ModifiersExpectations { address payable withdrawerWallet = payable(address(bytes20("withdrawerWallet"))); address payable fundWallet = payable(address(bytes20("fundWallet"))); - uint256[] public stakingDurations; + uint256 public constant OneMonth = 31 * 24 * 60 * 60; + LDYStaking.StakeDurationInfo[] public stakingDurationInfos; function setUp() public { // Deploy GenericERC20 (the $LDY token) @@ -75,14 +76,13 @@ contract Tests is Test, ModifiersExpectations { globalBlacklist.initialize(address(globalOwner)); vm.label(address(globalBlacklist), "GlobalBlacklist"); - uint256 oneMonth = 31 * 24 * 60 * 60; - stakingDurations = [ - 1 * oneMonth, - 6 * oneMonth, - 12 * oneMonth, - 24 * oneMonth, - 36 * oneMonth - ]; + stakingDurationInfos.push(LDYStaking.StakeDurationInfo(0 * OneMonth, 10000)); + stakingDurationInfos.push(LDYStaking.StakeDurationInfo(1 * OneMonth, 10000)); + stakingDurationInfos.push(LDYStaking.StakeDurationInfo(6 * OneMonth, 10000)); + stakingDurationInfos.push(LDYStaking.StakeDurationInfo(12 * OneMonth, 10000)); + stakingDurationInfos.push(LDYStaking.StakeDurationInfo(24 * OneMonth, 10000)); + stakingDurationInfos.push(LDYStaking.StakeDurationInfo(36 * OneMonth, 10000)); + // Deploy LDYStaking LDYStaking impl4 = new LDYStaking(); ERC1967Proxy proxy4 = new ERC1967Proxy(address(impl4), ""); @@ -92,8 +92,8 @@ contract Tests is Test, ModifiersExpectations { address(globalPause), address(globalBlacklist), address(ldyToken), - stakingDurations, - 12 * oneMonth, + stakingDurationInfos, + 12 * OneMonth, 1000 * 1e18 ); diff --git a/contracts/src/LDYStaking.sol b/contracts/src/LDYStaking.sol index 21faa3b3..95786b4e 100644 --- a/contracts/src/LDYStaking.sol +++ b/contracts/src/LDYStaking.sol @@ -39,6 +39,19 @@ contract LDYStaking is BaseUpgradeable, ReentrancyGuardUpgradeable { uint256 rewards; } + /** + * @notice Represent duration and multiplier per each stake option. + * @param duration Staking period in seconds. + * @param multiplier Token weight + */ + struct StakeDurationInfo { + uint256 duration; + uint256 multiplier; + } + + /// @notice Decimals of multiplier + uint256 public constant MULTIPLIER_BASIS = 1e4; + /// @notice Stake and Reward token. IERC20Upgradeable public stakeRewardToken; @@ -48,8 +61,8 @@ contract LDYStaking is BaseUpgradeable, ReentrancyGuardUpgradeable { /// @notice Minimal stake amount for perks. uint256 public stakeAmountForPerks; - /// @notice Stake durations. - uint256[] public stakeDurations; + /// @notice Stake durations info array. + StakeDurationInfo[] public stakeDurationInfos; /// @notice Duration of the rewards (in seconds). uint256 public rewardsDuration; @@ -69,6 +82,9 @@ contract LDYStaking is BaseUpgradeable, ReentrancyGuardUpgradeable { /// @notice Total staked amounts. uint256 public totalStaked; + // Total staked amounts with multiplier applied + uint256 public totalWeightedStake; + /// @notice User stakingInfo map, user address => array of the staking info mapping(address => StakingInfo[]) public userStakingInfo; @@ -122,7 +138,7 @@ contract LDYStaking is BaseUpgradeable, ReentrancyGuardUpgradeable { * @param globalPause_ The address of the GlobalPause contract. * @param globalBlacklist_ The address of the GlobalBlacklist contract. * @param stakeRewardToken_ The address of stake and reward token(LDY token). - * @param stakeDurations_ Available Staking Durations. + * @param stakeDurationInfos_ Available Staking Durations. * @param stakeDurationForPerks_ Minimal staking duration for perks. * @param stakeAmountForPerks_ Minimal staking amount for perks. */ @@ -131,13 +147,16 @@ contract LDYStaking is BaseUpgradeable, ReentrancyGuardUpgradeable { address globalPause_, address globalBlacklist_, address stakeRewardToken_, - uint256[] memory stakeDurations_, + StakeDurationInfo[] memory stakeDurationInfos_, uint256 stakeDurationForPerks_, uint256 stakeAmountForPerks_ ) public initializer { __Base_init(globalOwner_, globalPause_, globalBlacklist_); stakeRewardToken = IERC20Upgradeable(stakeRewardToken_); - stakeDurations = stakeDurations_; + uint stakeDurationInfosLength = stakeDurationInfos_.length; + for (uint256 i = 0; i < stakeDurationInfosLength; i++) { + stakeDurationInfos.push(stakeDurationInfos_[i]); + } stakeDurationForPerks = stakeDurationForPerks_; stakeAmountForPerks = stakeAmountForPerks_; } @@ -150,33 +169,35 @@ contract LDYStaking is BaseUpgradeable, ReentrancyGuardUpgradeable { * @notice Staked tokens cannot be withdrawn during the stakeDuration period and are eligible to claim rewards. * @dev Emits a `Staked` event upon successful staking. * @param amount The amount of tokens to stake. - * @param stakeDurationIndex The Index of stakeDurations array. + * @param stakeDurationIndex The Index of stakeDurationInfos array. */ function stake( uint256 amount, uint8 stakeDurationIndex ) external nonReentrant whenNotPaused notBlacklisted(_msgSender()) { require(amount > 0, "amount = 0"); - require(stakeDurationIndex <= stakeDurations.length - 1, "invalid staking period"); + require(stakeDurationIndex <= stakeDurationInfos.length - 1, "Invalid staking period"); _updateReward(address(0), 0); - uint256 stakeDuration = stakeDurations[stakeDurationIndex]; + StakeDurationInfo memory stakeDurationInfo = stakeDurationInfos[stakeDurationIndex]; StakingInfo memory stakingInfo = StakingInfo({ stakedAmount: amount, - unStakeAt: block.timestamp + stakeDuration, - duration: stakeDuration, + unStakeAt: block.timestamp + stakeDurationInfo.duration, + duration: stakeDurationInfo.duration, rewardPerTokenPaid: rewardPerTokenStored, rewards: 0 }); // check whether account is eligible for benefit from the protocol - if (stakeDuration >= stakeDurationForPerks && amount >= stakeAmountForPerks) { + if (stakeDurationInfo.duration >= stakeDurationForPerks && amount >= stakeAmountForPerks) { highTierAccounts[_msgSender()] = true; } userStakingInfo[_msgSender()].push(stakingInfo); uint256 stakeIndex = userStakingInfo[_msgSender()].length - 1; + uint256 weightedStake = (amount * stakeDurationInfo.multiplier) / MULTIPLIER_BASIS; + totalWeightedStake += weightedStake; totalStaked += amount; stakeRewardToken.safeTransferFrom(_msgSender(), address(this), amount); @@ -196,17 +217,23 @@ contract LDYStaking is BaseUpgradeable, ReentrancyGuardUpgradeable { uint256 stakeIndex ) external nonReentrant notBlacklisted(_msgSender()) { require(amount > 0, "amount = 0"); - require(userStakingInfo[_msgSender()].length >= stakeIndex + 1, "invalid stakeIndex"); + require(userStakingInfo[_msgSender()].length >= stakeIndex + 1, "Invalid stakeIndex"); require( block.timestamp >= userStakingInfo[_msgSender()][stakeIndex].unStakeAt, - "not allowed unstaking in the staking period" + "Cannot unstake during staking period" ); require( amount <= userStakingInfo[_msgSender()][stakeIndex].stakedAmount, - "insufficient amount" + "Insufficient unstake amount" ); _updateReward(_msgSender(), stakeIndex); + + uint256 multiplier = _getMultiplier(userStakingInfo[_msgSender()][stakeIndex].duration); + + uint256 currentWeightedStake = (amount * multiplier) / MULTIPLIER_BASIS; + totalWeightedStake -= currentWeightedStake; + totalStaked -= amount; userStakingInfo[_msgSender()][stakeIndex].stakedAmount -= amount; @@ -238,7 +265,7 @@ contract LDYStaking is BaseUpgradeable, ReentrancyGuardUpgradeable { * @param stakeIndex The index of user staking pool. */ function getReward(uint256 stakeIndex) external nonReentrant notBlacklisted(_msgSender()) { - require(userStakingInfo[_msgSender()].length >= stakeIndex + 1, "invalid stakeIndex"); + require(userStakingInfo[_msgSender()].length >= stakeIndex + 1, "Invalid stakeIndex"); _updateReward(_msgSender(), stakeIndex); _claimReward(_msgSender(), stakeIndex); } @@ -314,8 +341,8 @@ contract LDYStaking is BaseUpgradeable, ReentrancyGuardUpgradeable { return rewardPerTokenStored + - (rewardRatePerSec * (lastTimeRewardApplicable() - lastUpdateTime) * 1e18) / - totalStaked; + ((rewardRatePerSec * (lastTimeRewardApplicable() - lastUpdateTime) * 1e18) / + totalWeightedStake); } /** @@ -326,7 +353,9 @@ contract LDYStaking is BaseUpgradeable, ReentrancyGuardUpgradeable { */ function earned(address account, uint256 stakeIndex) public view returns (uint256) { StakingInfo memory userInfo = userStakingInfo[account][stakeIndex]; - uint256 rewardsSinceLastUpdate = ((userInfo.stakedAmount * + uint256 multiplier = _getMultiplier(userInfo.duration); + uint256 weightedAmount = (userInfo.stakedAmount * multiplier) / MULTIPLIER_BASIS; + uint256 rewardsSinceLastUpdate = ((weightedAmount * (rewardPerToken() - userInfo.rewardPerTokenPaid)) / 1e18); return rewardsSinceLastUpdate + userInfo.rewards; } @@ -397,6 +426,21 @@ contract LDYStaking is BaseUpgradeable, ReentrancyGuardUpgradeable { } } + /** + * @notice Get multiplier from stakeDurationInfo based on duration + * @param duration Stake Duration + */ + function _getMultiplier(uint256 duration) private view returns (uint256) { + uint256 stakeDurationInfosLength = stakeDurationInfos.length; + for (uint256 i = 0; i < stakeDurationInfosLength; i++) { + StakeDurationInfo memory stakeDurationInfo = stakeDurationInfos[i]; + if (duration == stakeDurationInfo.duration) { + return stakeDurationInfo.multiplier; + } + } + return 0; + } + /** * @notice Take minimum value between x and y. */