Skip to content
Closed
Changes from all commits
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
100 changes: 34 additions & 66 deletions plume/src/spin/Spin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ contract Spin is
PausableUpgradeable,
ReentrancyGuardUpgradeable
{

// Storage
struct UserData {
uint256 jackpotWins;
Expand Down Expand Up @@ -129,9 +128,10 @@ contract Spin is
// Note: Jackpot probability is handled by jackpotProbabilities based on dayOfWeek
rewardProbabilities = RewardProbabilities({
plumeTokenThreshold: 200_000, // Up to 200,000 (Approx 20%)
raffleTicketThreshold: 600_000, // Up to 600,000 (Approx 40%)
ppThreshold: 900_000 // Up to 900,000 (Approx 30%)
});
raffleTicketThreshold: 200_000, // Used to be up to 600,000 (Approx 40%). We have discontinued this tier by
// setting it equivalent to plumeTokenThreshold.
Comment on lines +131 to +132
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The existing tests in Spin.t.sol expect raffle ticket rewards to be awarded for probability values in the range 200_001 to 600_000 (see testRaffleTicketReward at line 719-739 and testBrokenStreakRaffleReward at line 598-640). With raffleTicketThreshold now set equal to plumeTokenThreshold, these tests will fail because the raffle ticket reward path is unreachable. The tests need to be updated to reflect the new probability distribution, either by removing raffle ticket test cases or updating them to test PP rewards instead.

Copilot uses AI. Check for mistakes.
Comment on lines +131 to +132
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

Setting raffleTicketThreshold equal to plumeTokenThreshold breaks the validation logic in the setRewardProbabilities function (line 527), which requires strict inequality (plumeTokenThreshold < raffleTicketThreshold). This will prevent administrators from updating reward probabilities after deployment. Additionally, with these values being equal, the condition on line 299 (probability <= rewardProbabilities.raffleTicketThreshold) will be evaluated for probabilities where probability <= 200_000, which are already captured by line 296-298. This means the "Raffle Ticket" reward path becomes unreachable. If the intention is to discontinue raffle tickets, consider using a value slightly greater than plumeTokenThreshold (e.g., 200_001) to satisfy the validation while ensuring the range is effectively empty, or remove the raffle ticket logic entirely.

Suggested change
raffleTicketThreshold: 200_000, // Used to be up to 600,000 (Approx 40%). We have discontinued this tier by
// setting it equivalent to plumeTokenThreshold.
raffleTicketThreshold: 200_001, // Previously up to 600,000 (Approx 40%). We have effectively discontinued
// this tier by setting it just above plumeTokenThreshold to satisfy validation while leaving the
// practical raffle range empty.

Copilot uses AI. Check for mistakes.
ppThreshold: 900_000 // Up to 900,000 (Approx 70%)
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The comment states PP threshold is "Up to 900,000 (Approx 70%)" but this calculation appears incorrect. If the raffle ticket threshold is effectively discontinued by setting it equal to plumeTokenThreshold at 200_000, then the PP range would be from 200_001 to 900_000, which is 700_000 out of 1_000_000, or exactly 70%. However, the jackpot probabilities also consume part of the range (varying by day from 1 to 20 based on jackpotProbabilities). The actual PP probability would be approximately (700_000 - avg_jackpot_threshold) / 1_000_000. Consider clarifying this comment to account for the dynamic jackpot threshold.

Copilot uses AI. Check for mistakes.
});
// Above 900,000 is "Nothing" (Approx 10%)
}

Expand Down Expand Up @@ -276,10 +276,11 @@ contract Spin is
* @notice Determines the reward category based on the VRF random number.
* @param randomness The random number generated by the Supra Router.
*/
function determineReward(
uint256 randomness,
uint256 streakForReward
) internal view returns (string memory, uint256) {
function determineReward(uint256 randomness, uint256 streakForReward)
internal
view
returns (string memory, uint256)
{
uint256 probability = randomness % 1_000_000; // Normalize VRF range to 1M

// Determine the current week in the 12-week campaign
Expand Down Expand Up @@ -338,9 +339,7 @@ contract Spin is
}

/// @notice Returns the user\'s current streak count based on their last spin date.
function currentStreak(
address user
) public view returns (uint256) {
function currentStreak(address user) public view returns (uint256) {
return _computeStreak(user, block.timestamp, false);
}

Expand Down Expand Up @@ -387,14 +386,11 @@ contract Spin is
* @param month2 The month of the second date.
* @param day2 The day of the second date.
*/
function isSameDay(
uint16 year1,
uint8 month1,
uint8 day1,
uint16 year2,
uint8 month2,
uint8 day2
) internal pure returns (bool) {
function isSameDay(uint16 year1, uint8 month1, uint8 day1, uint16 year2, uint8 month2, uint8 day2)
internal
pure
returns (bool)
{
return (year1 == year2 && month1 == month2 && day1 == day2);
}

Expand All @@ -403,9 +399,7 @@ contract Spin is
* @notice Gets the data for a user.
* @param user The address of the wallet.
*/
function getUserData(
address user
)
function getUserData(address user)
external
view
returns (
Expand Down Expand Up @@ -463,9 +457,7 @@ contract Spin is
// Setters
/// @notice Sets the jackpot probabilities for each day of the week.
/// @param _jackpotProbabilities An array of 7 integers representing the jackpot vrf range for each day.
function setJackpotProbabilities(
uint8[7] memory _jackpotProbabilities
) external onlyRole(ADMIN_ROLE) {
function setJackpotProbabilities(uint8[7] memory _jackpotProbabilities) external onlyRole(ADMIN_ROLE) {
jackpotProbabilities = _jackpotProbabilities;
}

Expand All @@ -476,65 +468,49 @@ contract Spin is
jackpotPrizes[week] = prize;
}

function setCampaignStartDate(
uint256 start
) external onlyRole(ADMIN_ROLE) {
function setCampaignStartDate(uint256 start) external onlyRole(ADMIN_ROLE) {
campaignStartDate = start == 0 ? block.timestamp : start;
}

/// @notice Sets the base value for raffle.
/// @param _baseRaffleMultiplier The base value for raffle.
function setBaseRaffleMultiplier(
uint256 _baseRaffleMultiplier
) external onlyRole(ADMIN_ROLE) {
function setBaseRaffleMultiplier(uint256 _baseRaffleMultiplier) external onlyRole(ADMIN_ROLE) {
baseRaffleMultiplier = _baseRaffleMultiplier;
}

/// @notice Sets the PP gained per spin.
/// @param _PP_PerSpin The PP gained per spin.
function setPP_PerSpin(
uint256 _PP_PerSpin
) external onlyRole(ADMIN_ROLE) {
function setPP_PerSpin(uint256 _PP_PerSpin) external onlyRole(ADMIN_ROLE) {
PP_PerSpin = _PP_PerSpin;
}

/// @notice Sets the Plume Token amounts.
/// @param _plumeAmounts An array of 3 integers representing the Plume Token amounts.
function setPlumeAmounts(
uint256[3] memory _plumeAmounts
) external onlyRole(ADMIN_ROLE) {
function setPlumeAmounts(uint256[3] memory _plumeAmounts) external onlyRole(ADMIN_ROLE) {
plumeAmounts = _plumeAmounts;
}

/// @notice Sets the Raffle contract address.
/// @param _raffleContract The address of the Raffle contract.
function setRaffleContract(
address _raffleContract
) external onlyRole(ADMIN_ROLE) {
function setRaffleContract(address _raffleContract) external onlyRole(ADMIN_ROLE) {
raffleContract = _raffleContract;
}

/// @notice Whitelist address to bypass cooldown period.
/// @param user The address of the user to whitelist.
function whitelist(
address user
) external onlyRole(ADMIN_ROLE) {
function whitelist(address user) external onlyRole(ADMIN_ROLE) {
whitelists[user] = true;
}

/// @notice Remove address from whitelist, restoring the daily spin limit.
/// @param user The address of the user to remove from the whitelist.
function removeWhitelist(
address user
) external onlyRole(ADMIN_ROLE) {
function removeWhitelist(address user) external onlyRole(ADMIN_ROLE) {
whitelists[user] = false;
}

/// @notice Enable or disable spinning
/// @param _enableSpin The flag to enable/disable spinning
function setEnableSpin(
bool _enableSpin
) external onlyRole(ADMIN_ROLE) {
function setEnableSpin(bool _enableSpin) external onlyRole(ADMIN_ROLE) {
enableSpin = _enableSpin;
}

Expand All @@ -544,11 +520,10 @@ contract Spin is
* @param _raffleTicketThreshold The upper threshold for Raffle Ticket rewards.
* @param _ppThreshold The upper threshold for PP rewards.
*/
function setRewardProbabilities(
uint256 _plumeTokenThreshold,
uint256 _raffleTicketThreshold,
uint256 _ppThreshold
) external onlyRole(ADMIN_ROLE) {
function setRewardProbabilities(uint256 _plumeTokenThreshold, uint256 _raffleTicketThreshold, uint256 _ppThreshold)
external
onlyRole(ADMIN_ROLE)
{
require(_plumeTokenThreshold < _raffleTicketThreshold, "Invalid thresholds order");
require(_raffleTicketThreshold < _ppThreshold, "Invalid thresholds order");
require(_ppThreshold <= 1_000_000, "Threshold exceeds maximum");
Expand All @@ -570,9 +545,7 @@ contract Spin is
* @notice Allows the admin to set the price required to spin.
* @param _newPrice The new price in wei.
*/
function setSpinPrice(
uint256 _newPrice
) external onlyRole(ADMIN_ROLE) {
function setSpinPrice(uint256 _newPrice) external onlyRole(ADMIN_ROLE) {
spinPrice = _newPrice;
}

Expand All @@ -582,9 +555,7 @@ contract Spin is
* which would otherwise leave the user's spin in a permanently pending state.
* @param user The address of the user whose pending spin should be canceled.
*/
function cancelPendingSpin(
address user
) external onlyRole(ADMIN_ROLE) {
function cancelPendingSpin(address user) external onlyRole(ADMIN_ROLE) {
require(isSpinPending[user], "No spin pending for this user");

uint256 nonce = pendingNonce[user];
Expand All @@ -602,7 +573,7 @@ contract Spin is
/// @notice Transfers Plume tokens safely, reverting if the contract has insufficient balance.
function _safeTransferPlume(address payable _to, uint256 _amount) internal {
require(address(this).balance >= _amount, "insufficient Plume in the Spin contract");
(bool success,) = _to.call{ value: _amount }("");
(bool success,) = _to.call{value: _amount}("");
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The spacing in the call syntax has been changed from "call{ value: _amount }" to "call{value: _amount}". This is inconsistent with the codebase convention where other files use a space after the opening brace (e.g., PlumeStakingRewardTreasury.sol:179, ManagementFacet.sol:156, StakingFacet.sol:416, AddressUtils.sol:27, MockPUSD.sol:101). For consistency, this should remain as "call{ value: _amount }".

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ungaro do i have to change my forge version or something? whe ni run forget fmt it changes the existing format of the file

require(success, "Plume transfer failed");
}

Expand All @@ -611,11 +582,8 @@ contract Spin is
* @notice Authorizes the upgrade of the contract.
* @param newImplementation The address of the new implementation.
*/
function _authorizeUpgrade(
address newImplementation
) internal override onlyRole(ADMIN_ROLE) { }
function _authorizeUpgrade(address newImplementation) internal override onlyRole(ADMIN_ROLE) {}

/// @notice Fallback function to receive ether
receive() external payable { }

receive() external payable {}
}