Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/QueryTypeStakerFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,22 @@ contract QueryTypeStakerFactory is Ownable {
/// @param _queryType The bit field representing the queries this pool will support.
/// @param _poolOwner The address that will own the staking pool.
/// @param _initialEntry The initial conversion table entry for the pool.
/// @param _decayRate The decay rate for the stake.
/// @return _poolAddress The address of the newly created staking pool.
/// @dev Only callable by the contract owner.
function createStakingPool(bytes32 _queryType, address _poolOwner, bytes32 _initialEntry)
external
returns (address _poolAddress)
{
function createStakingPool(
bytes32 _queryType,
address _poolOwner,
bytes32 _initialEntry,
uint8 _decayRate
) external returns (address _poolAddress) {
_checkOwner();
if (queryTypePools[_queryType] != address(0)) revert QueryTypeStakerFactory__PoolExists();

// Deploy new staking pool with STAKING_TOKEN address and initial conversion entry
QueryTypeStakingPool _newPool =
new QueryTypeStakingPool(_poolOwner, address(STAKING_TOKEN), address(this), _initialEntry);
QueryTypeStakingPool _newPool = new QueryTypeStakingPool(
_poolOwner, address(STAKING_TOKEN), address(this), _initialEntry, _decayRate
);
_poolAddress = address(_newPool);

// Store the pool address
Expand Down
27 changes: 26 additions & 1 deletion src/QueryTypeStakingPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ import {QueryTypeStakerFactory} from "src/QueryTypeStakerFactory.sol";
contract QueryTypeStakingPool is Ownable {
using SafeERC20 for IERC20;

/// @notice The decay rate applied to stake amounts. This rate determines what
/// percentage of the continuously decaying stake is lost as fees.
/// @dev Expressed as an integer from 0 to 100:
/// - 0: No decay (0% lost as fees)
/// - 100: Complete decay (100% lost as fees)
/// - 50: 50% decay rate (50% lost as fees)
/// @dev The decay is applied continuously over time, with users losing stake at a rate
/// proportional to the time elapsed since their last claim. The DECAY_RATE determines
/// what portion of this decayed amount is lost as fees.
uint8 public immutable DECAY_RATE;
Comment thread
thelostone-mc marked this conversation as resolved.

/// @notice The duration in seconds that tokens will be locked after staking. During this period
/// tokens cannot be withdrawn.
uint48 public lockupPeriod = 30 days;
Expand Down Expand Up @@ -106,6 +117,9 @@ contract QueryTypeStakingPool is Ownable {
/// @notice Emitted when decayed stake is claimed and forwarded to the fee recipient.
event DecayClaimed(address indexed staker, uint256 amount, address indexed feeRecipient);

/// @notice Thrown when attempting to set a decay rate outside the allowed range.
error QueryTypeStakingPool__InvalidDecayRate();

/// @notice Thrown when attempting to stake with an invalid lockup period.
error QueryTypeStakingPool__LockupPeriodTooLow();

Expand Down Expand Up @@ -149,15 +163,20 @@ contract QueryTypeStakingPool is Ownable {
/// @param _stakingToken The address of the ERC20 token that will be staked.
/// @param _factory The address of the factory that deployed this pool.
/// @param _initialConversionTableEntry The first entry in the conversion table history.
/// @param _decayRate The decay rate for the stake.
constructor(
address _owner,
address _stakingToken,
address _factory,
bytes32 _initialConversionTableEntry
bytes32 _initialConversionTableEntry,
uint8 _decayRate
) Ownable(_owner) {
STAKING_TOKEN = IERC20(_stakingToken);
FACTORY = _factory;

if (_decayRate > 100) revert QueryTypeStakingPool__InvalidDecayRate();
DECAY_RATE = _decayRate;

// Initialize the conversion table with the provided entry
conversionTableHistory.push(_initialConversionTableEntry);
emit ConversionTableUpdated(_initialConversionTableEntry);
Expand Down Expand Up @@ -335,6 +354,12 @@ contract QueryTypeStakingPool is Ownable {
if (totalPeriod == 0) return 0;

uint256 decayed = (stakeInfo.amount * elapsed) / totalPeriod;

// Apply proportional fee loss based on DECAY_RATE.
// DECAY_RATE represents the % of the decayed amount that should be lost as fees.
// Example: DECAY_RATE = 80 → lose 80% of the decayed amount as fees.
decayed = (decayed * DECAY_RATE) / 100;

if (decayed == 0) return 0;

if (decayed > stakeInfo.amount) decayed = stakeInfo.amount;
Expand Down
36 changes: 23 additions & 13 deletions test/QueryTypeStakerFactory.t.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.26;

import {Test, console2} from "forge-std/Test.sol";
import {Test} from "forge-std/Test.sol";
import {QueryTypeStakerFactory} from "src/QueryTypeStakerFactory.sol";
import {QueryTypeStakingPool} from "src/QueryTypeStakingPool.sol";
import {VmSafe} from "forge-std/Vm.sol";
Expand All @@ -20,9 +20,8 @@ contract QueryTypeStakerFactoryTest is Test {
queryType = bytes32(uint256(1));
}

function _createPool() internal returns (address) {
vm.prank(owner);
return factory.createStakingPool(queryType, owner, bytes32(0));
function _assumeSafeDecayRate(uint8 _decayRate) internal pure returns (uint8) {
return uint8(bound(_decayRate, 0, 100));
}
}

Expand Down Expand Up @@ -58,26 +57,33 @@ contract CreateStakingPool is QueryTypeStakerFactoryTest {
function testFuzz_CreatesNewStakingPoolWithArbitraryQueryType(
bytes32 _queryType,
address _poolOwner,
bytes32 _initialEntry
bytes32 _initialEntry,
uint8 _decayRate
) public {
_decayRate = _assumeSafeDecayRate(_decayRate);
vm.assume(_poolOwner != address(0));
vm.prank(owner);
address poolAddress = factory.createStakingPool(_queryType, _poolOwner, _initialEntry);
address poolAddress =
factory.createStakingPool(_queryType, _poolOwner, _initialEntry, _decayRate);

assertTrue(poolAddress != address(0));
assertEq(factory.queryTypePools(_queryType), poolAddress);
assertEq(QueryTypeStakingPool(poolAddress).owner(), _poolOwner);
assertEq(QueryTypeStakingPool(poolAddress).DECAY_RATE(), _decayRate);
}

function testFuzz_EmitsCreateQueryTypeStakingPoolEventWithArbitraryQueryType(
bytes32 _queryType,
address _poolOwner,
bytes32 _initialEntry
bytes32 _initialEntry,
uint8 _decayRate
) public {
_decayRate = _assumeSafeDecayRate(_decayRate);
vm.assume(_poolOwner != address(0));
vm.recordLogs();
vm.prank(owner);
address poolAddress = factory.createStakingPool(_queryType, _poolOwner, _initialEntry);
address poolAddress =
factory.createStakingPool(_queryType, _poolOwner, _initialEntry, _decayRate);

VmSafe.Log[] memory entries = vm.getRecordedLogs();
assertEq(entries[2].topics[0], keccak256("CreateQueryTypeStakingPool(bytes32,address)"));
Expand All @@ -88,27 +94,31 @@ contract CreateStakingPool is QueryTypeStakerFactoryTest {
function testFuzz_RevertIf_CallerIsNotOwner(
address _notOwner,
address _poolOwner,
bytes32 _initialEntry
bytes32 _initialEntry,
uint8 _decayRate
) public {
_decayRate = _assumeSafeDecayRate(_decayRate);
vm.assume(_notOwner != owner && _notOwner != address(0));
vm.assume(_poolOwner != address(0));

vm.prank(_notOwner);
vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", _notOwner));
factory.createStakingPool(queryType, _poolOwner, _initialEntry);
factory.createStakingPool(queryType, _poolOwner, _initialEntry, _decayRate);
}

function testFuzz_RevertIf_PoolAlreadyExistsWithArbitraryQueryType(
bytes32 _queryType,
address _poolOwner,
bytes32 _initialEntry
bytes32 _initialEntry,
uint8 _decayRate
) public {
_decayRate = _assumeSafeDecayRate(_decayRate);
vm.assume(_poolOwner != address(0));
vm.startPrank(owner);
factory.createStakingPool(_queryType, _poolOwner, _initialEntry);
factory.createStakingPool(_queryType, _poolOwner, _initialEntry, _decayRate);

vm.expectRevert(QueryTypeStakerFactory.QueryTypeStakerFactory__PoolExists.selector);
factory.createStakingPool(_queryType, _poolOwner, _initialEntry);
factory.createStakingPool(_queryType, _poolOwner, _initialEntry, _decayRate);
vm.stopPrank();
}
}
Expand Down
Loading
Loading