Skip to content

Commit 776de71

Browse files
committed
feat: implement DECAY_RATE
1 parent a678ffd commit 776de71

4 files changed

Lines changed: 157 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: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import {QueryTypeStakerFactory} from "src/QueryTypeStakerFactory.sol";
1616
contract QueryTypeStakingPool is Ownable {
1717
using SafeERC20 for IERC20;
1818

19+
/// @notice The percentage of the stake that will decay over the full lockup + access period.
20+
/// @dev Must be between 0 (no decay) and 100 (fully decayed by the end of access period).
21+
uint8 public immutable DECAY_RATE;
22+
1923
/// @notice The duration in seconds that tokens will be locked after staking. During this period
2024
/// tokens cannot be withdrawn.
2125
uint48 public lockupPeriod = 30 days;
@@ -106,6 +110,9 @@ contract QueryTypeStakingPool is Ownable {
106110
/// @notice Emitted when decayed stake is claimed and forwarded to the fee recipient.
107111
event DecayClaimed(address indexed staker, uint256 amount, address indexed feeRecipient);
108112

113+
/// @notice Thrown when attempting to set a decay rate outside the allowed range.
114+
error QueryTypeStakingPool__InvalidDecayRate();
115+
109116
/// @notice Thrown when attempting to stake with an invalid lockup period.
110117
error QueryTypeStakingPool__LockupPeriodTooLow();
111118

@@ -149,15 +156,20 @@ contract QueryTypeStakingPool is Ownable {
149156
/// @param _stakingToken The address of the ERC20 token that will be staked.
150157
/// @param _factory The address of the factory that deployed this pool.
151158
/// @param _initialConversionTableEntry The first entry in the conversion table history.
159+
/// @param _decayRate The decay rate for the stake.
152160
constructor(
153161
address _owner,
154162
address _stakingToken,
155163
address _factory,
156-
bytes32 _initialConversionTableEntry
164+
bytes32 _initialConversionTableEntry,
165+
uint8 _decayRate
157166
) Ownable(_owner) {
158167
STAKING_TOKEN = IERC20(_stakingToken);
159168
FACTORY = _factory;
160169

170+
if (_decayRate > 100) revert QueryTypeStakingPool__InvalidDecayRate();
171+
DECAY_RATE = _decayRate;
172+
161173
// Initialize the conversion table with the provided entry
162174
conversionTableHistory.push(_initialConversionTableEntry);
163175
emit ConversionTableUpdated(_initialConversionTableEntry);
@@ -335,6 +347,9 @@ contract QueryTypeStakingPool is Ownable {
335347
if (totalPeriod == 0) return 0;
336348

337349
uint256 decayed = (stakeInfo.amount * elapsed) / totalPeriod;
350+
// Scale by configured decay percentage
351+
decayed = (decayed * DECAY_RATE) / 100;
352+
338353
if (decayed == 0) return 0;
339354

340355
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
}

test/QueryTypeStakingPool.t.sol

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@ contract QueryTypeStakingPoolTest is Test {
2525

2626
factory.setFeeRecipient(feeRecipient);
2727

28-
bytes32 queryType = bytes32(uint256(1));
29-
bytes32 initialEntry = bytes32(uint256(1));
30-
address poolAddress = factory.createStakingPool(queryType, address(this), initialEntry);
28+
address poolAddress = factory.createStakingPool(
29+
bytes32(uint256(1)), // queryType
30+
address(this), // poolOwner
31+
bytes32(uint256(1)), // initialEntry
32+
100 // decayRate
33+
);
3134
pool = QueryTypeStakingPool(poolAddress);
3235

3336
stakingToken.mint(staker, INITIAL_BALANCE);
@@ -61,10 +64,23 @@ contract Constructor is QueryTypeStakingPoolTest {
6164
vm.assume(_stakingToken != address(0));
6265

6366
QueryTypeStakingPool _newPool =
64-
new QueryTypeStakingPool(_owner, _stakingToken, address(factory), _initialEntry);
67+
new QueryTypeStakingPool(_owner, _stakingToken, address(factory), _initialEntry, 0);
6568
assertEq(address(_newPool.STAKING_TOKEN()), _stakingToken);
6669
assertEq(_newPool.conversionTableHistory(0), _initialEntry);
6770
}
71+
72+
function testFuzz_RevertIf_DecayRateIsInvalid(uint8 _decayRate) public {
73+
_decayRate = uint8(bound(_decayRate, 101, type(uint8).max));
74+
75+
vm.expectRevert(QueryTypeStakingPool.QueryTypeStakingPool__InvalidDecayRate.selector);
76+
new QueryTypeStakingPool(
77+
address(this), // owner
78+
address(stakingToken), // stakingToken
79+
address(factory), // factory
80+
bytes32(uint256(100)), // initialEntry
81+
_decayRate
82+
);
83+
}
6884
}
6985

7086
contract UpdateConversionTable is QueryTypeStakingPoolTest {
@@ -1030,3 +1046,91 @@ contract Claim is QueryTypeStakingPoolTest {
10301046
);
10311047
}
10321048
}
1049+
1050+
contract DecayRate is QueryTypeStakingPoolTest {
1051+
function testFuzz_DecayRateAppliedCorrectly(
1052+
uint8 _decayRate,
1053+
uint256 _stakeAmount,
1054+
uint32 _timeElapsed
1055+
) public {
1056+
// Bound parameters to reasonable ranges
1057+
_decayRate = uint8(bound(_decayRate, 1, 100)); // Valid decay rates only
1058+
_stakeAmount = bound(_stakeAmount, 100 ether, 10_000 ether);
1059+
_timeElapsed = uint32(bound(_timeElapsed, 1 days, 60 days));
1060+
1061+
// Create pool with fuzzed decay rate
1062+
address poolAddress = factory.createStakingPool(
1063+
bytes32(uint256(5)), // queryType
1064+
address(this), // poolOwner
1065+
bytes32(uint256(100)), // initialEntry
1066+
_decayRate // fuzzed decay rate
1067+
);
1068+
QueryTypeStakingPool fuzzedPool = QueryTypeStakingPool(poolAddress);
1069+
1070+
// Set capacity based on stake amount
1071+
uint256 capacity = bound(10_000 ether, _stakeAmount, _stakeAmount * 10);
1072+
fuzzedPool.setStakingTokenCapacity(capacity);
1073+
1074+
// Setup staker
1075+
stakingToken.mint(staker, INITIAL_BALANCE);
1076+
vm.prank(staker);
1077+
stakingToken.approve(address(fuzzedPool), type(uint256).max);
1078+
1079+
// Stake tokens
1080+
vm.prank(staker);
1081+
fuzzedPool.stake(_stakeAmount);
1082+
1083+
uint256 initialAmount = _stakeAmount;
1084+
1085+
// Advance time
1086+
vm.warp(block.timestamp + _timeElapsed);
1087+
1088+
// Claim decay
1089+
fuzzedPool.claim(staker);
1090+
1091+
// Verify decay behavior
1092+
(uint256 finalAmount,,,,,) = fuzzedPool.stakes(staker);
1093+
1094+
// All valid decay rates should result in some decay
1095+
assertLe(finalAmount, initialAmount, "Amount should not increase with decay");
1096+
assertGe(finalAmount, 0, "Amount should not go negative");
1097+
1098+
// Higher decay rates should result in more decay
1099+
if (_decayRate > 50) {
1100+
assertLt(finalAmount, initialAmount, "High decay rate should cause significant decay");
1101+
}
1102+
}
1103+
1104+
function testFuzz_DecayRateBoundaries(uint8 _decayRate) public {
1105+
// Test boundary conditions for decay rate
1106+
_decayRate = uint8(bound(_decayRate, 1, 100));
1107+
1108+
// Create pool with boundary decay rate
1109+
address poolAddress = factory.createStakingPool(
1110+
bytes32(uint256(6)), // queryType
1111+
address(this), // poolOwner
1112+
bytes32(uint256(100)), // initialEntry
1113+
_decayRate // boundary decay rate
1114+
);
1115+
QueryTypeStakingPool boundaryPool = QueryTypeStakingPool(poolAddress);
1116+
1117+
// Verify decay rate was set correctly
1118+
assertEq(boundaryPool.DECAY_RATE(), _decayRate, "Decay rate should be set correctly");
1119+
1120+
// Test that pool functions work with boundary decay rates
1121+
stakingToken.mint(staker, INITIAL_BALANCE);
1122+
vm.prank(staker);
1123+
stakingToken.approve(address(boundaryPool), type(uint256).max);
1124+
1125+
// Set capacity
1126+
boundaryPool.setStakingTokenCapacity(10_000 ether);
1127+
1128+
// Stake tokens
1129+
vm.prank(staker);
1130+
boundaryPool.stake(1000 ether);
1131+
1132+
// Verify stake was successful
1133+
(uint256 amount,,,,,) = boundaryPool.stakes(staker);
1134+
assertEq(amount, 1000 ether, "Stake should work with boundary decay rate");
1135+
}
1136+
}

0 commit comments

Comments
 (0)