Skip to content

Commit

Permalink
Add claim window get/set, add natspec comt
Browse files Browse the repository at this point in the history
  • Loading branch information
vivinvinh212 committed Jan 23, 2025
1 parent 0f49ddc commit d8cb750
Showing 1 changed file with 100 additions and 43 deletions.
143 changes: 100 additions & 43 deletions src/SwanLottery.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,98 +3,155 @@ pragma solidity ^0.8.20;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Swan} from "./Swan.sol";
import {SwanAgent} from "./SwanAgent.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";

contract SwanLottery is Ownable {
// Base const
// CONSTANTS
uint256 public constant BASIS_POINTS = 10000;

struct MultiplierConfig {
uint256 multiplier;
uint256 probability;
}

// STORAGE
Swan public immutable swan;
ERC20 public immutable token;

// Storage for multipliers and claims
mapping(address => mapping(uint256 => uint256)) public artifactMultipliers; // artifact => round => multiplier
mapping(address => mapping(uint256 => bool)) public rewardsClaimed; // artifact => round => claimed
mapping(address => bool) public isAuthorized;
// number of rounds after listing that rewards can be claimed
uint256 public claimWindow;

modifier onlyAuthorized() {
require(isAuthorized[msg.sender], "Not authorized");
_;
}
// lottery data
mapping(address artifact => mapping(uint256 round => uint256 multiplier)) public artifactMultipliers;
mapping(address artifact => mapping(uint256 round => bool claimed)) public rewardsClaimed;
mapping(address addr => bool isAllowed) public authorized;

// EVENTS
event AuthorizationUpdated(address indexed addr, bool status);
event MultiplierAssigned(address indexed artifact, uint256 indexed round, uint256 multiplier);
event RewardClaimed(address indexed seller, address indexed artifact, uint256 indexed round, uint256 reward);
event AuthorizationUpdated(address indexed authorizer, bool status);
event ClaimWindowUpdated(uint256 oldWindow, uint256 newWindow);

// ERRORS
error Unauthorized(address caller);
error InvalidClaimWindow();
error MultiplierAlreadyAssigned(address artifact, uint256 round);
error InvalidRound(uint256 current, uint256 required);
error RewardAlreadyClaimed(address artifact, uint256 round);
error ClaimWindowExpired(uint256 currentRound, uint256 listingRound, uint256 window);

constructor(address _swan, address initialOwner) Ownable(initialOwner) {
// MODIFIERS
modifier onlyAuthorized() {
if (!authorized[msg.sender]) revert Unauthorized(msg.sender);
_;
}

/// @notice Constructor sets initial configuration
/// @dev Sets Swan contract, token, and initial claim window
constructor(address _swan, uint256 _claimWindow) Ownable(msg.sender) {
swan = Swan(_swan);
token = swan.token();
authorized[msg.sender] = true;
claimWindow = _claimWindow;
}

/// @notice Assigns multiplier to a newly listed artifact
function assignMultiplier(address artifact, uint256 round) external onlyAuthorized {
// verify listing exists
Swan.ArtifactListing memory listing = swan.getListing(artifact);
require(listing.seller != address(0), "Invalid artifact");
require(listing.round == round, "Wrong round");
require(artifactMultipliers[artifact][round] == 0, "Already assigned");
if (listing.seller == address(0)) revert("Invalid artifact");
if (listing.round != round) revert InvalidRound(listing.round, round);

// check multiplier not already assigned
if (artifactMultipliers[artifact][round] != 0) {
revert MultiplierAlreadyAssigned(artifact, round);
}

// compute and store multiplier
uint256 randomness = _computeRandomness(artifact, round);
uint256 multiplier = _selectMultiplier(randomness);

uint256 multiplier = _computeMultiplier(artifact, round);
artifactMultipliers[artifact][round] = multiplier;
emit MultiplierAssigned(artifact, round, multiplier);
}

function _computeMultiplier(address artifact, uint256 round) internal view returns (uint256) {
// Utilize previous round's data
/// @notice Public view of multiplier computation
function computeMultiplier(address artifact, uint256 round) public view returns (uint256) {
return _selectMultiplier(_computeRandomness(artifact, round));
}

/// @notice Compute randomness for multiplier
function _computeRandomness(address artifact, uint256 round) internal view returns (uint256) {
bytes32 randomness = blockhash(block.number - 1);
uint256 rand = uint256(
return uint256(
keccak256(
abi.encodePacked(
randomness, artifact, round, swan.getListing(artifact).seller, swan.getListing(artifact).agent
)
)
) % BASIS_POINTS;
}

// example
if (rand < 7500) return 1 * BASIS_POINTS; // 75% chance of 1x
if (rand < 9000) return 2 * BASIS_POINTS; // 15% chance of 2x
if (rand < 9500) return 3 * BASIS_POINTS; // 5% chance of 3x
if (rand < 9800) return 5 * BASIS_POINTS; // 3% chance of 5x
if (rand < 9950) return 10 * BASIS_POINTS; // 1.5% chance of 10x
return 20 * BASIS_POINTS; // 0.5% chance of 20x
/// @notice Select multiplier based on random value
function _selectMultiplier(uint256 rand) public pure returns (uint256) {
// 75% chance of 1x
if (rand < 7500) return BASIS_POINTS;
// 15% chance of 2x
if (rand < 9000) return 2 * BASIS_POINTS;
// 5% chance of 3x
if (rand < 9500) return 3 * BASIS_POINTS;
// 3% chance of 5x
if (rand < 9800) return 5 * BASIS_POINTS;
// 1.5% chance of 10x
if (rand < 9950) return 10 * BASIS_POINTS;
// 0.5% chance of 20x
return 20 * BASIS_POINTS;
}

function claimRewards(address artifactAddress, uint256 round) public onlyAuthorized {
require(!rewardsClaimed[artifactAddress][round], "Already claimed");
/// @notice Claims rewards for sold artifacts within claim window
function claimRewards(address artifact, uint256 round) public onlyAuthorized {
// Check not already claimed
if (rewardsClaimed[artifact][round]) revert RewardAlreadyClaimed(artifact, round);

// Get listing and validate
Swan.ArtifactListing memory listing = swan.getListing(artifact);
if (listing.status != Swan.ArtifactStatus.Sold) revert("Not sold");
if (listing.round != round) revert InvalidRound(listing.round, round);

// Check claim window using agent's round
(uint256 currentRound,,) = SwanAgent(listing.agent).getRoundPhase();
if (currentRound > listing.round + claimWindow) {
revert ClaimWindowExpired(currentRound, listing.round, claimWindow);
}

// Get listing and compute randomness/multiplier
Swan.ArtifactListing memory listing = swan.getListing(artifactAddress);
require(listing.status == Swan.ArtifactStatus.Sold, "Not sold");
require(listing.round == round, "Wrong round");
// Check multiplier and compute reward
uint256 multiplier = artifactMultipliers[artifact][round];
if (multiplier <= BASIS_POINTS) revert("No bonus available");

uint256 reward = getRewards(artifactAddress, round);
uint256 reward = getRewards(artifact, round);
if (reward > 0) {
rewardsClaimed[artifactAddress][round] = true;
// Transfer reward from platform fees to seller
rewardsClaimed[artifact][round] = true;
token.transferFrom(swan.owner(), listing.seller, reward);
emit RewardClaimed(listing.seller, artifactAddress, round, reward);
emit RewardClaimed(listing.seller, artifact, round, reward);
}
}

// Check potential reward
/// @notice Calculate potential reward
function getRewards(address artifact, uint256 round) public view returns (uint256) {
Swan.ArtifactListing memory listing = swan.getListing(artifact);
uint256 multiplier = artifactMultipliers[artifact][round];

return (listing.listingFee * multiplier) / BASIS_POINTS;
}

/// @notice Update authorization status
function setAuthorization(address addr, bool status) external onlyOwner {
isAuthorized[addr] = status;
authorized[addr] = status;
emit AuthorizationUpdated(addr, status);
}

/// @notice Update claim window
/// @dev Only owner can call
function setClaimWindow(uint256 newWindow) external onlyOwner {
if (newWindow == 0) revert InvalidClaimWindow();
uint256 oldWindow = claimWindow;
claimWindow = newWindow;
emit ClaimWindowUpdated(oldWindow, newWindow);
}
}

0 comments on commit d8cb750

Please sign in to comment.