Skip to content

Commit

Permalink
Fix tracking the last collected interval (#137)
Browse files Browse the repository at this point in the history
* test: reproduce the issue

* fix: solve the issue with staking and paying a reward at the same interval

* fix: better same interval check

* doc: fix a typo

* test: update test summary

* chore: upgrade testnets with the fix
  • Loading branch information
amarinkovic authored Jun 17, 2024
1 parent 2b17b22 commit 7edd82d
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 24 deletions.
32 changes: 16 additions & 16 deletions gemforge.deployments.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,22 +175,22 @@
"chainId": 11155111,
"contracts": [
{
"name": "PhasedDiamondCutFacet",
"fullyQualifiedName": "PhasedDiamondCutFacet.sol:PhasedDiamondCutFacet",
"name": "StakingFacet",
"fullyQualifiedName": "StakingFacet.sol:StakingFacet",
"sender": "0x931c3aC09202650148Edb2316e97815f904CF4fa",
"txHash": "0xb25af109873f94eaa99ab3b5ef8a4c7d95a003abfe9f89012464b1cc5eb8ffa3",
"txHash": "0xccaab243e581df5c6379844ea0de825578fe5dddfabe01a1b12567b0589a4e09",
"onChain": {
"address": "0x0639a051BD87E19dB7534b759de7c95d6e285a6A",
"address": "0x9981C2f530F3D0DD3a226ff3dd99EC08970A2b9E",
"constructorArgs": []
}
},
{
"name": "StakingFacet",
"fullyQualifiedName": "StakingFacet.sol:StakingFacet",
"name": "PhasedDiamondCutFacet",
"fullyQualifiedName": "PhasedDiamondCutFacet.sol:PhasedDiamondCutFacet",
"sender": "0x931c3aC09202650148Edb2316e97815f904CF4fa",
"txHash": "0x966e5a97b6a36607792d50d79cd586a78ae33f8bbea3740348d7064fe9672254",
"txHash": "0xb25af109873f94eaa99ab3b5ef8a4c7d95a003abfe9f89012464b1cc5eb8ffa3",
"onChain": {
"address": "0xed9F49c6f4Aea18dc079C54DE84bB5DFF3babE88",
"address": "0x0639a051BD87E19dB7534b759de7c95d6e285a6A",
"constructorArgs": []
}
},
Expand Down Expand Up @@ -895,22 +895,22 @@
"chainId": 84532,
"contracts": [
{
"name": "PhasedDiamondCutFacet",
"fullyQualifiedName": "PhasedDiamondCutFacet.sol:PhasedDiamondCutFacet",
"name": "StakingFacet",
"fullyQualifiedName": "StakingFacet.sol:StakingFacet",
"sender": "0x931c3aC09202650148Edb2316e97815f904CF4fa",
"txHash": "0x2aa2ad32dcf222f2af7c68374adef9c204ce8c2dc21a4d28034c3d9b0dca141d",
"txHash": "0x821b74a88828be096d4b8a4562be8096a9d30cfca39a5d89ad82dcc8db83e61f",
"onChain": {
"address": "0x4DFf560895F62eCae213c4AED8A76fD6a4C41276",
"address": "0xC5B096055237aA5E045f0AB842165B48a4058028",
"constructorArgs": []
}
},
{
"name": "StakingFacet",
"fullyQualifiedName": "StakingFacet.sol:StakingFacet",
"name": "PhasedDiamondCutFacet",
"fullyQualifiedName": "PhasedDiamondCutFacet.sol:PhasedDiamondCutFacet",
"sender": "0x931c3aC09202650148Edb2316e97815f904CF4fa",
"txHash": "0xd3423acf487ccfc97e08a42ff9a68f7c6b9bd0416a8e9397d165d7d434a90d2f",
"txHash": "0x2aa2ad32dcf222f2af7c68374adef9c204ce8c2dc21a4d28034c3d9b0dca141d",
"onChain": {
"address": "0x5a84Aca1447cD6f14447cb0F76d14b473fE8ff3B",
"address": "0x4DFf560895F62eCae213c4AED8A76fD6a4C41276",
"constructorArgs": []
}
},
Expand Down
16 changes: 13 additions & 3 deletions src/libs/LibTokenizedVaultStaking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ library LibTokenizedVaultStaking {
0
);
rewards.amounts[currencyIndex] += userDistributionAmount;
// last interval the reward was paid out, but before the one provided in the input
rewards.lastPaidInterval = i;
}
}
Expand All @@ -264,12 +265,21 @@ library LibTokenizedVaultStaking {

bytes32 tokenId = s.stakingConfigs[_entityId].tokenId;

(StakingState memory state, RewardsBalances memory rewards) = _getStakingStateWithRewardsBalances(_stakerId, _entityId, _interval);
StakingState memory state;
RewardsBalances memory rewards;

(state, rewards) = _getStakingStateWithRewardsBalances(_stakerId, _entityId, _interval);
if (rewards.currencies.length > 0) {
bytes32 vTokenId = _vTokenId(_entityId, tokenId, _interval);
if (rewards.lastPaidInterval < _interval) {
// we must update the stake collected for the user, to the interval when that reward was actually paid out, not the current one
// also update the state and boosts up to that interval, not later than that, that is why we make this call again with different interval
// so that we can calculate the boosted amounts up to the desired interval
(state, rewards) = _getStakingStateWithRewardsBalances(_stakerId, _entityId, rewards.lastPaidInterval);
}
bytes32 vTokenId = _vTokenId(_entityId, tokenId, rewards.lastPaidInterval);

// Update state
s.stakeCollected[_entityId][_stakerId] = _interval;
s.stakeCollected[_entityId][_stakerId] = rewards.lastPaidInterval;
s.stakeBoost[vTokenId][_stakerId] = state.boost;
s.stakeBalance[vTokenId][_stakerId] = state.balance;

Expand Down
101 changes: 96 additions & 5 deletions test/T06Staking.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,97 @@ contract T06Staking is D03ProtocolDefaults {
// printAppstorage();
}

/**
* [40] Bob stakes 100 NAYM => 100/100
* [40] Sue stakes 100 NAYM => 100/200
* [70] NLF pays reward1: 1000 USDC (Bob: 100, Sue: 100, Total: 200)
* [100] Sue stakes 100 NAYM => 100/300 (collects: 50% reward1)
* [100] Bob stakes 100 NAYM => 100/400 (collects: 50% reward1)
* [100] NLF pays reward2: 1000 USDC (Bob: 200, Sue: 200, Total: 400)
*/
function test_stakeAndPayRewardAtSameInterval() public {
uint256 stake100 = 100e18;
uint256 reward1000usdc = 1_000_000000;

assertEq(nayms.internalBalanceOf(bob.entityId, usdcId), usdcBalance[bob.entityId], "Bob's USDC balance should be zero");
assertEq(nayms.internalBalanceOf(sue.entityId, usdcId), usdcBalance[sue.entityId], "Sue's USDC balance should be zero");

assertStakedAmount(bob.entityId, 0, "Bob's staked amount [1] should be zero");
assertStakedAmount(sue.entityId, 0, "Sue's staked amount [1] should be zero");

uint256 startStaking = block.timestamp + 100 days;
initStaking(startStaking);

vm.warp(startStaking + 40 days);
c.log("\n ~ START Staking\n".blue());

startPrank(bob);
nayms.stake(nlf.entityId, stake100);
assertStakedAmount(bob.entityId, stake100, "Bob's staked amount [1] should increase");
c.log("~ [%s] Bob staked 100 NAYM".blue(), currentInterval());
printCurrentState(nlf.entityId, bob.entityId, "Bob");

startPrank(sue);
nayms.stake(nlf.entityId, stake100);
assertStakedAmount(sue.entityId, stake100, "Sue's staked amount [1] should increase");
c.log("~ [%s] Sue staked 100 NAYM".blue(), currentInterval());
printCurrentState(nlf.entityId, sue.entityId, "Sue");

vm.warp(startStaking + 70 days);

startPrank(nlf);
nayms.payReward(makeId(LC.OBJECT_TYPE_STAKING_REWARD, bytes20("reward1")), nlf.entityId, usdcId, reward1000usdc);
c.log("~ [%s] NLF payed out reward1: 1000 USDC".blue(), currentInterval());

printCurrentState(nlf.entityId, bob.entityId, "Bob");
printCurrentState(nlf.entityId, sue.entityId, "Sue");

vm.warp(startStaking + 100 days);

startPrank(sue);
nayms.stake(nlf.entityId, stake100);
assertStakedAmount(sue.entityId, stake100 * 2, "Sue's staked amount [2] should increase");
usdcBalance[sue.entityId] += reward1000usdc / 2;
assertEq(nayms.internalBalanceOf(sue.entityId, usdcId), usdcBalance[sue.entityId], "Sue's USDC balance should increase");
c.log("~ [%s] Sue staked 100 NAYM (collects 50% reward1)".blue(), currentInterval());

printCurrentState(nlf.entityId, sue.entityId, "Sue");
c.log(" Sue's USDC: %s".green(), nayms.internalBalanceOf(sue.entityId, usdcId) / 1e6);
printCurrentState(nlf.entityId, bob.entityId, "Bob");

startPrank(bob);
nayms.stake(nlf.entityId, stake100);
assertStakedAmount(bob.entityId, 2 * stake100, "Bob's staked amount [2] should increase");
usdcBalance[bob.entityId] += reward1000usdc / 2;
assertEq(nayms.internalBalanceOf(bob.entityId, usdcId), usdcBalance[bob.entityId], "Bob's USDC balance should increase");
c.log("~ [%s] Bob staked 100 NAYM (collects 50% reward1)".blue(), currentInterval());

printCurrentState(nlf.entityId, bob.entityId, "Bob");
c.log(" Bob's USDC: %s".green(), nayms.internalBalanceOf(bob.entityId, usdcId) / 1e6);
printCurrentState(nlf.entityId, sue.entityId, "Sue");

startPrank(nlf);
nayms.payReward(makeId(LC.OBJECT_TYPE_STAKING_REWARD, bytes20("reward2")), nlf.entityId, usdcId, reward1000usdc);
c.log("~ [%s] NLF payed out reward2: 1000 USDC".blue(), currentInterval());

printCurrentState(nlf.entityId, bob.entityId, "Bob");
printCurrentState(nlf.entityId, sue.entityId, "Sue");

{
uint256 totalStakedAmount = 2 * stake100;

uint256 sueBoost = (calculateBoost(40 days, 100 days, R, I, SCALE_FACTOR) * stake100) / totalStakedAmount;
uint256 bobBoost = (calculateBoost(40 days, 100 days, R, I, SCALE_FACTOR) * stake100) / totalStakedAmount;
uint256 totalBoost = sueBoost + bobBoost;

uint256 sueReward = (reward1000usdc * sueBoost) / totalBoost;
uint256 bobReward = (reward1000usdc * bobBoost) / totalBoost;

assertEq(getRewards(bob.entityId, nlf.entityId), bobReward, "Bob's reward [3] should increase");
assertEq(getRewards(sue.entityId, nlf.entityId), sueReward, "Sue's reward [3] should increase");
}
}

function printAppstorage() public {
uint64 interval = currentInterval() + 2;

Expand All @@ -1168,11 +1259,11 @@ contract T06Staking is D03ProtocolDefaults {
}
c.log();

c.log(" -- Lou --");
for (uint64 i = 0; i <= interval; i++) {
logStateAt(i, lou.entityId, nlf.entityId);
}
c.log();
// c.log(" -- Lou --");
// for (uint64 i = 0; i <= interval; i++) {
// logStateAt(i, lou.entityId, nlf.entityId);
// }
// c.log();

c.log(" -- NLF --");
for (uint64 i = 0; i <= interval; i++) {
Expand Down

0 comments on commit 7edd82d

Please sign in to comment.