Skip to content

Commit 2a00a69

Browse files
Allow decay rate to be adjustable on deployment (#13)
--------- Co-authored-by: keating <keating.dev@protonmail.com>
1 parent 9b81f7c commit 2a00a69

4 files changed

Lines changed: 256 additions & 24 deletions

File tree

src/QueryTypeStakerFactory.sol

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,22 @@ contract QueryTypeStakerFactory is Ownable {
4848
/// @param _queryType The bit field representing the queries this pool will support.
4949
/// @param _poolOwner The address that will own the staking pool.
5050
/// @param _initialEntry The initial conversion table entry for the pool.
51+
/// @param _decayRate The decay rate for the stake.
5152
/// @return _poolAddress The address of the newly created staking pool.
5253
/// @dev Only callable by the contract owner.
53-
function createStakingPool(bytes32 _queryType, address _poolOwner, bytes32 _initialEntry)
54-
external
55-
returns (address _poolAddress)
56-
{
54+
function createStakingPool(
55+
bytes32 _queryType,
56+
address _poolOwner,
57+
bytes32 _initialEntry,
58+
uint8 _decayRate
59+
) external returns (address _poolAddress) {
5760
_checkOwner();
5861
if (queryTypePools[_queryType] != address(0)) revert QueryTypeStakerFactory__PoolExists();
5962

6063
// Deploy new staking pool with STAKING_TOKEN address and initial conversion entry
61-
QueryTypeStakingPool _newPool =
62-
new QueryTypeStakingPool(_poolOwner, address(STAKING_TOKEN), address(this), _initialEntry);
64+
QueryTypeStakingPool _newPool = new QueryTypeStakingPool(
65+
_poolOwner, address(STAKING_TOKEN), address(this), _initialEntry, _decayRate
66+
);
6367
_poolAddress = address(_newPool);
6468

6569
// Store the pool address

src/QueryTypeStakingPool.sol

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ import {QueryTypeStakerFactory} from "src/QueryTypeStakerFactory.sol";
1616
contract QueryTypeStakingPool is Ownable {
1717
using SafeERC20 for IERC20;
1818

19+
/// @notice The decay rate applied to stake amounts. This rate determines what
20+
/// percentage of the continuously decaying stake is lost as fees.
21+
/// @dev Expressed as an integer from 0 to 100:
22+
/// - 0: No decay (0% lost as fees)
23+
/// - 100: Complete decay (100% lost as fees)
24+
/// - 50: 50% decay rate (50% lost as fees)
25+
/// @dev The decay is applied continuously over time, with users losing stake at a rate
26+
/// proportional to the time elapsed since their last claim. The DECAY_RATE determines
27+
/// what portion of this decayed amount is lost as fees.
28+
uint8 public immutable DECAY_RATE;
29+
1930
/// @notice The duration in seconds that tokens will be locked after staking. During this period
2031
/// tokens cannot be withdrawn.
2132
uint48 public lockupPeriod = 30 days;
@@ -106,6 +117,9 @@ contract QueryTypeStakingPool is Ownable {
106117
/// @notice Emitted when decayed stake is claimed and forwarded to the fee recipient.
107118
event DecayClaimed(address indexed staker, uint256 amount, address indexed feeRecipient);
108119

120+
/// @notice Thrown when attempting to set a decay rate outside the allowed range.
121+
error QueryTypeStakingPool__InvalidDecayRate();
122+
109123
/// @notice Thrown when attempting to stake with an invalid lockup period.
110124
error QueryTypeStakingPool__LockupPeriodTooLow();
111125

@@ -149,15 +163,20 @@ contract QueryTypeStakingPool is Ownable {
149163
/// @param _stakingToken The address of the ERC20 token that will be staked.
150164
/// @param _factory The address of the factory that deployed this pool.
151165
/// @param _initialConversionTableEntry The first entry in the conversion table history.
166+
/// @param _decayRate The decay rate for the stake.
152167
constructor(
153168
address _owner,
154169
address _stakingToken,
155170
address _factory,
156-
bytes32 _initialConversionTableEntry
171+
bytes32 _initialConversionTableEntry,
172+
uint8 _decayRate
157173
) Ownable(_owner) {
158174
STAKING_TOKEN = IERC20(_stakingToken);
159175
FACTORY = _factory;
160176

177+
if (_decayRate > 100) revert QueryTypeStakingPool__InvalidDecayRate();
178+
DECAY_RATE = _decayRate;
179+
161180
// Initialize the conversion table with the provided entry
162181
conversionTableHistory.push(_initialConversionTableEntry);
163182
emit ConversionTableUpdated(_initialConversionTableEntry);
@@ -319,6 +338,12 @@ contract QueryTypeStakingPool is Ownable {
319338
if (totalPeriod == 0) return 0;
320339

321340
uint256 decayed = (stakeInfo.amount * elapsed) / totalPeriod;
341+
342+
// Apply proportional fee loss based on DECAY_RATE.
343+
// DECAY_RATE represents the % of the decayed amount that should be lost as fees.
344+
// Example: DECAY_RATE = 80 → lose 80% of the decayed amount as fees.
345+
decayed = (decayed * DECAY_RATE) / 100;
346+
322347
if (decayed == 0) return 0;
323348

324349
if (decayed > stakeInfo.amount) decayed = stakeInfo.amount;

test/QueryTypeStakerFactory.t.sol

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: Apache-2.0
22
pragma solidity 0.8.26;
33

4-
import {Test, console2} from "forge-std/Test.sol";
4+
import {Test} from "forge-std/Test.sol";
55
import {QueryTypeStakerFactory} from "src/QueryTypeStakerFactory.sol";
66
import {QueryTypeStakingPool} from "src/QueryTypeStakingPool.sol";
77
import {VmSafe} from "forge-std/Vm.sol";
@@ -20,9 +20,8 @@ contract QueryTypeStakerFactoryTest is Test {
2020
queryType = bytes32(uint256(1));
2121
}
2222

23-
function _createPool() internal returns (address) {
24-
vm.prank(owner);
25-
return factory.createStakingPool(queryType, owner, bytes32(0));
23+
function _assumeSafeDecayRate(uint8 _decayRate) internal pure returns (uint8) {
24+
return uint8(bound(_decayRate, 0, 100));
2625
}
2726
}
2827

@@ -58,26 +57,33 @@ contract CreateStakingPool is QueryTypeStakerFactoryTest {
5857
function testFuzz_CreatesNewStakingPoolWithArbitraryQueryType(
5958
bytes32 _queryType,
6059
address _poolOwner,
61-
bytes32 _initialEntry
60+
bytes32 _initialEntry,
61+
uint8 _decayRate
6262
) public {
63+
_decayRate = _assumeSafeDecayRate(_decayRate);
6364
vm.assume(_poolOwner != address(0));
6465
vm.prank(owner);
65-
address poolAddress = factory.createStakingPool(_queryType, _poolOwner, _initialEntry);
66+
address poolAddress =
67+
factory.createStakingPool(_queryType, _poolOwner, _initialEntry, _decayRate);
6668

6769
assertTrue(poolAddress != address(0));
6870
assertEq(factory.queryTypePools(_queryType), poolAddress);
6971
assertEq(QueryTypeStakingPool(poolAddress).owner(), _poolOwner);
72+
assertEq(QueryTypeStakingPool(poolAddress).DECAY_RATE(), _decayRate);
7073
}
7174

7275
function testFuzz_EmitsCreateQueryTypeStakingPoolEventWithArbitraryQueryType(
7376
bytes32 _queryType,
7477
address _poolOwner,
75-
bytes32 _initialEntry
78+
bytes32 _initialEntry,
79+
uint8 _decayRate
7680
) public {
81+
_decayRate = _assumeSafeDecayRate(_decayRate);
7782
vm.assume(_poolOwner != address(0));
7883
vm.recordLogs();
7984
vm.prank(owner);
80-
address poolAddress = factory.createStakingPool(_queryType, _poolOwner, _initialEntry);
85+
address poolAddress =
86+
factory.createStakingPool(_queryType, _poolOwner, _initialEntry, _decayRate);
8187

8288
VmSafe.Log[] memory entries = vm.getRecordedLogs();
8389
assertEq(entries[2].topics[0], keccak256("CreateQueryTypeStakingPool(bytes32,address)"));
@@ -88,27 +94,31 @@ contract CreateStakingPool is QueryTypeStakerFactoryTest {
8894
function testFuzz_RevertIf_CallerIsNotOwner(
8995
address _notOwner,
9096
address _poolOwner,
91-
bytes32 _initialEntry
97+
bytes32 _initialEntry,
98+
uint8 _decayRate
9299
) public {
100+
_decayRate = _assumeSafeDecayRate(_decayRate);
93101
vm.assume(_notOwner != owner && _notOwner != address(0));
94102
vm.assume(_poolOwner != address(0));
95103

96104
vm.prank(_notOwner);
97105
vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", _notOwner));
98-
factory.createStakingPool(queryType, _poolOwner, _initialEntry);
106+
factory.createStakingPool(queryType, _poolOwner, _initialEntry, _decayRate);
99107
}
100108

101109
function testFuzz_RevertIf_PoolAlreadyExistsWithArbitraryQueryType(
102110
bytes32 _queryType,
103111
address _poolOwner,
104-
bytes32 _initialEntry
112+
bytes32 _initialEntry,
113+
uint8 _decayRate
105114
) public {
115+
_decayRate = _assumeSafeDecayRate(_decayRate);
106116
vm.assume(_poolOwner != address(0));
107117
vm.startPrank(owner);
108-
factory.createStakingPool(_queryType, _poolOwner, _initialEntry);
118+
factory.createStakingPool(_queryType, _poolOwner, _initialEntry, _decayRate);
109119

110120
vm.expectRevert(QueryTypeStakerFactory.QueryTypeStakerFactory__PoolExists.selector);
111-
factory.createStakingPool(_queryType, _poolOwner, _initialEntry);
121+
factory.createStakingPool(_queryType, _poolOwner, _initialEntry, _decayRate);
112122
vm.stopPrank();
113123
}
114124
}

0 commit comments

Comments
 (0)