Skip to content

Add Decay to stake amount#4

Merged
alexkeating merged 3 commits into
mainfrom
decay-stake
Jul 10, 2025
Merged

Add Decay to stake amount#4
alexkeating merged 3 commits into
mainfrom
decay-stake

Conversation

@mikeghen

@mikeghen mikeghen commented May 14, 2025

Copy link
Copy Markdown

Resolves: #2

To do:

@mikeghen
mikeghen changed the base branch from main to query-key-change May 14, 2025 19:50
@mikeghen
mikeghen marked this pull request as draft May 14, 2025 19:50
@mikeghen
mikeghen force-pushed the decay-stake branch 4 times, most recently from 35faa00 to 0fd8b2f Compare May 14, 2025 20:58
@mikeghen
mikeghen force-pushed the query-key-change branch 2 times, most recently from a80768d to 4093bad Compare May 19, 2025 13:34
Comment thread src/QueryTypeStakingPool.sol Outdated

/// @notice The period over which stake fully decays to zero. After this period, the entire
/// original stake amount is considered decayed and is claimable by the fee recipient.
uint256 public constant DECAY_PERIOD = 365 days;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decay period should match accessEnd

Comment thread src/QueryTypeStakingPool.sol Outdated
/// @notice Convenience overload to claim for msg.sender.
function claim() external {
_claimDecay(msg.sender);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the use case for this function? I can't see an incentive to call it.

Comment thread src/QueryTypeStakingPool.sol Outdated

// Fee collected from jailed amount for blocked addresses
if (isBlocklisted[_staker]) totalJailed -= decayed;
else totalStaked -= decayed;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decayed stake should not affect capacity. I am not sure exactly how this affects the rest of the code, but we probably need to track the individual stake capacity seperately on the stakeInfo.

@mikeghen
mikeghen changed the base branch from query-key-change to main May 27, 2025 18:59
@mikeghen
mikeghen marked this pull request as ready for review May 27, 2025 19:00
@mikeghen
mikeghen force-pushed the decay-stake branch 8 times, most recently from b5d5369 to fe64913 Compare May 29, 2025 18:21
Comment thread src/QueryTypeStakerFactory.sol Outdated
if (_stakingToken == address(0)) revert QueryTypeStakerFactory__InvalidTokenAddress();
STAKING_TOKEN = IERC20(_stakingToken);
feeRecipient = _owner;
emit FeeRecipientUpdated(address(0), _owner);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably abstract this to a internal function that is used here and in setFeeRecipient

Comment thread src/QueryTypeStakingPool.sol Outdated

// Reset lockup and access periods
StakeInfo memory stakeInfo = stakes[msg.sender];
StakeInfo storage stakeInfo = stakes[msg.sender];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be storage?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it didn't need to be

Comment thread src/QueryTypeStakingPool.sol Outdated

// If there is no fee recipient yet, update lastClaimed and exit without modifying
if (_feeRecipient == address(0)) {
stakeInfo.lastClaimed = uint48(block.timestamp);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would last claimed need to be updated?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't need to be, shouldn't be, now it only updates when the decay gets applied

Comment thread test/QueryTypeStakerFactory.t.sol Outdated
new QueryTypeStakerFactory(_owner, address(0));
}

function testFuzz_EmitsFeeRecipientUpdatedEventWithArbitraryRecipient(address _newRecipient)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also have a test for emitting the event when the contract is created

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update this to test its in the constructor and added a seperate test contract for the setFeeRecipient function

Comment thread test/QueryTypeStakingPool.t.sol Outdated
returns (uint256 boundedTimeSkip)
{
// Ensure some stake remains after decay by capping time skip below access period end
return bound(_timeSkip, pool.lockupPeriod() + 1, pool.lockupPeriod() + pool.accessPeriod() - 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stake can decay during the lockup

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this to just the Unstake contract where we need it to skip into the time where it's unstakable

Comment thread test/QueryTypeStakingPool.t.sol Outdated
assertEq(pool.totalStaked(), _stakeAmount, "Total staked incorrect");
}

function testFuzz_ClaimOnBehalfSuccessfully(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function testFuzz_ClaimOnBehalfSuccessfully(
function testFuzz_ClaimArbitraryDecaySuccessfully(

Comment thread test/QueryTypeStakingPool.t.sol Outdated
assertEq(remaining.amount, _stakeAmount - decayed);
}

function test_ClaimWithNoStakeDoesNothing(address _nonStaker) public {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function test_ClaimWithNoStakeDoesNothing(address _nonStaker) public {
function testFuzz_ClaimWithNoStakeDoesNothing(address _nonStaker) public {

Comment thread test/QueryTypeStakingPool.t.sol Outdated
assertEq(pool.totalStaked(), 0);
}

function testFuzz_ClaimWhenFeeRecipientUnsetDoesNothing(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function testFuzz_ClaimWhenFeeRecipientUnsetDoesNothing(
function testFuzz_ClaimWhenFeeRecipientIsTheZeroAddrss(

Comment thread test/QueryTypeStakingPool.t.sol Outdated
assertEq(stakeAfter.lastClaimed, uint48(block.timestamp), "lastClaimed not updated");
}

function testFuzz_ClaimWhenBlockedAndFeeRecipientUnsetKeepsJail(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name is a little jumbled. We should try and make it clearer

assertEq(lockupEnd, expectedLockupEnd);
assertEq(accessEnd, expectedAccessEnd);
assertEq(stakeInfo.lockupEnd, expectedLockupEnd);
assertEq(stakeInfo.accessEnd, expectedAccessEnd);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we assert the decayed value as well?

Comment thread src/QueryTypeStakingPool.sol Outdated
if (decayed == 0) return 0;

address _feeRecipient = QueryTypeStakerFactory(FACTORY).feeRecipient();
if (_feeRecipient == address(0)) return 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there may be a problem here. If no decay is applied while the fee recipient is the 0 address than once a fee recipient is added many stakes may have immediately decayed to 0. Since the lastClaimed will be when the stake was created. We should probably just remove this and burn the decayed stake if the fee recipient is zero. That way at least the behavior is consistent.

@github-actions

Copy link
Copy Markdown

Coverage after merging decay-stake into main will be

100.00%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src
   QueryTypeStakerFactory.sol100%100%100%100%
   QueryTypeStakingPool.sol100%100%100%100%

@alexkeating
alexkeating merged commit 9c0d8cd into main Jul 10, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Decay to stake amount

2 participants