Address
0x9eb00a1e41159f9a938003369080d1e40200ffceCurrent Holdings
$0.8957
TXs sent
not counted
First Active
2026-03-23
block 26,098,278
Last Active
40 days ago
block 27,212,572
Net worth historyi
27 snapshots · to block 27,212,599
exact matchGAME_CONTRACTsolc 0.8.20+commit.a1b79de6runtime exact · creation exact
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// Contract Name: GAME_CONTRACT
/*
============================================================
PEWPEW RACING SYSTEM — CONTRACTS OVERVIEW
============================================================
1. UPGRADE_REGISTRY
- Canonical source of all upgrade definitions
- Defines slots, stats, pricing, and paint upgrades
2. RACER_UPGRADES_1155
- ERC1155 inventory holding all upgrade tokens
- Validates upgrade IDs against UpgradeRegistry
- Enforces mint/burn permissions
3. CAR_NFT
- ERC721 representing player-owned cars
- Stores base car identity and ownership
4. GAME_CONTRACT (THIS CONTRACT)
- Core gameplay logic
- Applies upgrades, runs races, enforces rules
5. GAME_VIEWS (GAME_VIEWS.sol)
- Visuals helper (side/top manifests)
- Aux storage + helpers to keep GAME_CONTRACT under the 24KB code-size limit
(stores replays; hosts swap helper + burn-selection helper)
- Only the configured GAME_CONTRACT can write (set via `setGame()`)
6. UPGRADE_SHOP
- Handles purchasing and minting of upgrades
- Only contract allowed to mint upgrade tokens
7. CAR_SHOP
- Handles car minting and acquisition
- Entry point for new players
*/
interface IERC20 {
function balanceOf(address who) external view returns (uint256);
}
interface IERC20Burnable {
function burn(uint256 amount) external;
}
interface IPulseXRouterV2 {
function swapExactETHForTokens(
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external payable returns (uint[] memory amounts);
function WPLS() external pure returns (address);
}
interface IRaceCarNFT {
function ownerOf(uint256 tokenId) external view returns (address);
function cars(uint256 tokenId)
external
view
returns (
uint32 modelId,
uint8 rarity,
uint16 baseSpeed,
uint16 baseHandling,
uint16 baseAccel
);
}
interface IUpgradeRegistry {
function slotCount() external view returns (uint8);
function exists(uint256 id) external view returns (bool);
function getStats(uint256 id) external view returns (int16 speed, int16 accel, int16 handling);
function getLevel(uint256 id) external view returns (uint8);
function getSlotType(uint256 id) external view returns (uint8);
}
interface IRacerUpgrades1155GameBurn {
function balanceOf(address account, uint256 id) external view returns (uint256);
function burnForGame(address from, uint256 id, uint256 amount) external;
}
interface IGAME_AUX {
function getSwapPath(uint8 mode) external view returns (address[] memory path);
function setSwapPath(uint8 mode, address[] calldata path) external;
function swapExactPlsForMode(uint8 mode, address router, uint16 slippageBps)
external
payable
returns (uint256 outReceived);
function storeDragRaceReplay(
uint256 raceId,
uint256 car1,
uint256 car2,
uint256 time1,
uint256 time2,
uint256 winnerCarId,
address winnerOwner,
address starter,
uint64 timestamp,
uint256 rewardPaid
) external;
function getDragRaceReplay(uint256 raceId)
external
view
returns (
uint256 car1,
uint256 car2,
uint256 time1,
uint256 time2,
uint256 winnerCarId,
address winnerOwner,
address starter,
uint64 timestamp,
uint256 rewardPaid
);
function storeSingleRaceBatchReplay(
uint256 raceId,
uint8 trackId,
uint256[] calldata cars,
uint256[] calldata times,
uint256 winnerCarId,
address winnerOwner,
uint256 winnerTime,
address starter,
uint64 timestamp,
uint256 rewardPaid
) external;
function getSingleRaceBatchReplay(uint256 raceId)
external
view
returns (
uint8 trackId,
uint8 carCount,
uint256 winnerCarId,
address winnerOwner,
uint256 winnerTime,
uint64 timestamp,
address starter,
uint256 rewardPaid
);
function getSingleRaceBatchCars(uint256 raceId) external view returns (uint256[] memory);
function getSingleRaceBatchTimes(uint256 raceId) external view returns (uint256[] memory);
function storeSeasonLeg(
uint256 seasonId,
uint8 leg,
uint8 trackId,
uint256 winnerCarId,
uint256 winnerTime,
uint256 racedAt
) external;
function getSeasonLeg(uint256 seasonId, uint8 leg)
external
view
returns (uint8 trackId, uint256 winnerCarId, uint256 winnerTime, uint256 racedAt);
function storeSeasonFinal(uint256 seasonId, uint256 winnerCarId, address winnerOwner, uint256 prizePaid) external;
function getSeasonFinal(uint256 seasonId) external view returns (uint256 winnerCarId, address winnerOwner, uint256 prizePaid);
function purgeSeason(uint256 seasonId) external;
function pickRandomHighestUpgrade(address player, uint256 carId) external view returns (uint256 upgradeId);
function pickRandomEquippedToken(address player, uint256 carId) external view returns (uint256 upgradeId);
function validateNoZeroNoDups(uint256[] calldata ids) external pure;
}
contract GAME_CONTRACT {
// -----------------------
// Custom errors (bytecode shrink)
// -----------------------
error Reentrancy();
error Paused();
error NotOwner();
error NewOwnerZero();
error TooHigh();
error MaxTracks();
error InvalidTrack();
error ToZero();
error TransferFailed();
error PlsSendFailed();
error VaultZero();
error NeedPewpew();
error NotCarOwner();
error InvalidUpgrade();
error NoToken();
error SlotTooLarge();
error NothingEquipped();
error WrongPlsAmount();
error SameCar();
error StarterMustOwnCar1();
error Need2Cars();
error TooManyCars();
error StarterMustOwnFirstCar();
error SeasonInactive();
error SeasonComplete();
error NoRoster();
error CallerPewpewTransferFailed();
error NotActive();
error StarterCarMismatch();
error BurnGreaterThanReceived();
error RewardTransferFailed();
error Need1MPewpew();
error SeasonLimit();
// V2
error InvalidRandomnessRange();
error SeasonLegCooldown();
error HelperNotSet();
error ViewsCallFailed();
// Selectors for external views helper
bytes4 private constant _SEL_GET_SIDE_VISUALS = 0xda4c4cf9; // getSideViewVisuals(address,uint256)
bytes4 private constant _SEL_GET_TOP_VISUALS = 0x81571583; // getTopViewVisuals(address,uint256)
// -----------------------
// Constants
// -----------------------
uint8 internal constant MAX_TRACKS = 55;
uint8 internal constant MAX_SLOTS = 64;
uint16 internal constant MAX_BPS = 10_000;
uint256 internal constant MAX_RACE_CARS = 32;
uint256 internal constant MAX_SINGLE_RACE_CARS = 20;
uint8 internal constant SEASON_LEGS = 5;
// -----------------------
// Reentrancy guard (no OZ)
// -----------------------
uint256 private _reentrancyStatus = 1;
modifier nonReentrant() {
if (_reentrancyStatus != 1) revert Reentrancy();
_reentrancyStatus = 2;
_;
_reentrancyStatus = 1;
}
// -----------------------
// Pause (races)
// -----------------------
bool public paused;
event PausedSet(bool paused);
modifier whenNotPaused() {
if (paused) revert Paused();
_;
}
// -----------------------
// Admin
// -----------------------
address internal owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
modifier onlyOwner() {
if (msg.sender != owner) revert NotOwner();
_;
}
// -----------------------
// External contracts
// -----------------------
IERC20 internal pewpew; // 18 decimals
IERC20 internal usdc; // 6 decimals
IERC20 internal pewswap5; // 18 decimals
IPulseXRouterV2 internal router;
IRaceCarNFT public carNFT;
IUpgradeRegistry public upgradeRegistry;
IRacerUpgrades1155GameBurn public upgrades1155;
// -----------------------
// Slippage config (PLS swaps)
// -----------------------
uint16 internal slippageBps = 300; // 3%
// -----------------------
// Vault defaults
// -----------------------
address internal constant DEFAULT_FARMERS_LPUSDC_VAULT =
0xe046d5921f68B7fB305e5c55269163E6e217a3b8;
address internal constant DEFAULT_BENEVOLENCE_VAULT =
0x6ee09dACF727E423A05568365C04664A0cAe171f;
address internal benevolenceVault;
address internal lpHarvesterVault;
// -----------------------
// Vault send (PEWPEW-gated)
// -----------------------
// Owner-configurable minimum PEWPEW required to call `holderSendToVault()`.
// Default: 1,000,000 PEWPEW.
uint256 public minPewpewToVaultBlast = 1_000_000 ether;
uint256 internal maxPewpewVaultSend = 5_000 ether;
uint256[5] internal vaultBlastTierThresholds = [
uint256(1_000_000 ether),
5_000_000 ether,
10_000_000 ether,
20_000_000 ether,
30_000_000 ether
];
uint16[5] internal vaultBlastTierRewardBps = [uint16(100), 200, 300, 400, 500];
// -----------------------
// Tracks
// -----------------------
uint8 public trackCount;
struct Track {
uint16 speedMult; // 1000 = 1.0x
uint16 accelMult; // 1000 = 1.0x
uint16 handlingMult; // 1000 = 1.0x
}
mapping(uint8 => Track) public tracks;
// V2: per-track randomness range (bps). If both are zero, no upset is applied.
mapping(uint8 => uint16) internal upsetChanceMinBps;
mapping(uint8 => uint16) internal upsetChanceMaxBps;
// -----------------------
// Upgrades (equip state)
// -----------------------
mapping(uint256 => mapping(uint256 => uint256)) public equippedUpgradeByCarSlot;
// -----------------------
// Drag best times
// -----------------------
mapping(uint256 => uint256) public bestDragTimeMsByCar;
// -----------------------
// V2: Drag race replay storage
// -----------------------
uint256 internal nextDragRaceId = 1;
// Replay data is stored in the external `aux` helper.
// -----------------------
// Seasons (aggregate: 5 legs)
// -----------------------
uint256 internal nextSeasonId = 1;
mapping(uint256 => mapping(uint256 => uint256)) public seasonTotalTimeMs;
uint256[] internal activeSeasons;
mapping(uint256 => bool) public isSeasonActive;
mapping(uint256 => uint8) public seasonRaceCount; // 0..5
mapping(uint256 => uint256[]) private _seasonCars;
// -----------------------
// Best track times
// -----------------------
mapping(uint256 => mapping(uint8 => uint256)) public bestTrackTimeMsByCarTrack;
// -----------------------
// Season randomness (instant hybrid entropy)
// -----------------------
uint256 internal seasonEntropyAccumulator;
mapping(uint256 => uint256) internal seasonRaceNonce;
// -----------------------
// Season configuration
// -----------------------
uint16 public seasonBurnBps = 500; // 5%
uint16 public seasonChampionRewardBps = 2000; // 20% of PEWSWAP5 balance at finalize
// Blast caller tiers (PEWPEW balance thresholds) for calling `blastSeasonRace`.
// Tiers: 1m / 5m / 10m / 20m / 30m.
uint256 internal constant SEASON_BLAST_TIER1 = 1_000_000 ether;
uint256 internal constant SEASON_BLAST_TIER2 = 5_000_000 ether;
uint256 internal constant SEASON_BLAST_TIER3 = 10_000_000 ether;
uint256 internal constant SEASON_BLAST_TIER4 = 20_000_000 ether;
uint256 internal constant SEASON_BLAST_TIER5 = 30_000_000 ether;
// Reward bps (of current contract PEWPEW balance at call time) for tiers 1..5.
// Defaults: 1% / 2% / 3% / 4% / 5%.
uint16[5] internal seasonBlastTierRewardBps = [uint16(100), 200, 300, 400, 500];
// Minimum PEWPEW required to call season blast (legs 2..5). Default: 1,000,000 PEWPEW.
uint256 public minPewpewToSeasonBlast = SEASON_BLAST_TIER1;
// V2: season leg cooldown (for legs 2..5)
uint32 internal seasonLegCooldownSeconds = 2 minutes;
mapping(uint256 => uint256) internal seasonNextLegAvailableAt;
// V2: owner-configured 5-track season sequence
uint8[SEASON_LEGS] internal seasonIncludedTracks;
// V2: per-leg stored results for replay are stored in the external `aux` helper.
// V2: optional season history purge to reduce long-term storage growth
uint256 internal seasonHistoryLimit = 20; // set to 0 to disable purging
uint256[] internal seasonHistory; // ended seasons in order
// -----------------------
// Fees (exact PLS required) — paid modes
// -----------------------
uint256 public ghostRaceFee = 200 ether;
uint256 public dragRaceFee = 400 ether;
uint256 public singleRaceFee = 800 ether;
uint256 public seasonStartFee = 100_000 ether;
// Starter win payouts (of the PEWSWAP5 received from the fee swap)
uint16 public dragStarterWinRewardBps = 2500; // 25%
uint16 public singleStarterWinRewardBps = 3500; // 35%
// -----------------------
// V2: Single race (multi-car) replay storage
// -----------------------
uint256 internal nextSingleRaceBatchId = 1;
// Replay data is stored in the external `aux` helper.
// -----------------------
// Events (gameplay)
// -----------------------
event DragRaceResult(uint256 car1, uint256 time1, uint256 car2, uint256 time2, address starter, bool starterWon);
event GhostRaceResult(uint256 carId, uint256 newTime, uint256 bestTimeBefore);
event SeasonStarted(
uint256 indexed seasonId,
address indexed scheduler,
uint256 indexed starterCarId,
uint256 rosterSize,
uint256 pewpewReceived,
uint256 pewpewBurned
);
event SeasonRaceLegResult(
uint256 indexed seasonId,
uint8 indexed legNumber,
uint8 indexed trackId,
address caller,
uint256 callerRewardPewpew,
uint256 winnerCarIdLeg,
uint256 winnerTimeLeg
);
event SeasonFinalized(
uint256 indexed seasonId,
uint8 indexed trackIdFinal,
uint256 winnerCarIdOverall,
address winnerOwner,
uint256 grandPrizePewswap5
);
event UpgradeEquipped(uint256 carId, uint8 slot, uint256 upgradeId);
event UpgradeUnequipped(uint256 carId, uint8 slot, uint256 upgradeId);
event UpgradeBurnedForLoss(address indexed player, uint256 carId, uint256 upgradeId);
// -----------------------
// Events (vault/rescue)
// -----------------------
// V2 events
event DragRaceReplayStored(uint256 indexed raceId, uint256 winnerCarId, address winnerOwner, uint256 rewardPaid);
event SingleRaceBatchStored(uint256 indexed raceId, uint8 indexed trackId, uint256 winnerCarId, address winnerOwner, uint256 winnerTime, uint256 rewardPaid);
event RewardSkipped(address indexed to, uint256 amountRequested);
event HolderVaultSend(address indexed caller, address indexed token, address indexed vault, uint256 amount);
event Rescue(address indexed token, address indexed to, uint256 amount);
// -----------------------
// Visual manifests (read-only)
// -----------------------
struct SideViewVisuals {
uint32 modelId;
uint256 paintUpgradeId;
uint256[] decalUpgradeIds;
bool hasSupercharger;
}
struct TopViewVisuals {
uint32 modelId;
uint256 paintUpgradeId;
uint256[] decalUpgradeIds;
bool hasSupercharger;
}
struct PendingEquipSelection {
uint8 slot;
uint8 level;
uint256 upgradeId;
}
// V2: external view helper contract (to keep GAME_CONTRACT deployable under code-size limit)
// Expects functions:
// - getSideViewVisuals(address game, uint256 carId) returns (uint32,uint256,uint256[],bool)
// - getTopViewVisuals(address game, uint256 carId) returns (uint32,uint256,uint256[],bool)
address internal views;
// V2: aux helper contract for replay storage (can be the same address as `views`)
address internal aux;
// -----------------------
// CONSTRUCTOR — NO INPUTS
// -----------------------
constructor() {
owner = msg.sender;
emit OwnershipTransferred(address(0), msg.sender);
// Core system contracts (defaults, owner adjustable)
carNFT = IRaceCarNFT(0x344222c442AC321887398419A6Be4e7F1dE56632);
upgradeRegistry = IUpgradeRegistry(0xCC8764cDfc9318fB1b50b46B47f0f036c32a793C);
upgrades1155 = IRacerUpgrades1155GameBurn(0xBdfd50B2Be6f1D578128905AABc657cDA3a8AD74);
// Economic / routing contracts (defaults, owner adjustable)
pewpew = IERC20(0x9deeaF046e144Fb6304A5ACD2aF142bBfE958030);
usdc = IERC20(0x15D38573d2feeb82e7ad5187aB8c1D52810B1f07);
router = IPulseXRouterV2(0x165C3410fC91EF562C50559f7d2289fEbed552d9);
// Rewards token (PEWSWAP5)
pewswap5 = IERC20(0xE318a5c4bFEBb23DA12A47ac429B8325B7D45B80);
benevolenceVault = DEFAULT_BENEVOLENCE_VAULT;
lpHarvesterVault = DEFAULT_FARMERS_LPUSDC_VAULT;
// Default season tracks: 0..4
seasonIncludedTracks[0] = 0;
seasonIncludedTracks[1] = 1;
seasonIncludedTracks[2] = 2;
seasonIncludedTracks[3] = 3;
seasonIncludedTracks[4] = 4;
_initLaunchTracks();
}
function setGameView(address a) external onlyOwner {
if (a == address(0)) revert ToZero();
views = a;
aux = a;
}
function _aux() internal view returns (address a) {
a = aux;
if (a == address(0)) revert HelperNotSet();
}
// -----------------------
// Receive PLS
// -----------------------
receive() external payable {}
// -----------------------
// Track init (5 maps live)
// Track IDs and defaults per your table:
// 0 Oval Speedway (Speed, 7–9%)
// 1 Wavy Speedway (Handling, 10–12%)
// 2 Mountain Pass (Accel, 12–15%)
// 3 Oceanfront Raceway (Speed+Handling, 8–10%)
// 4 Rounded Triangle Circuit (Handling, 9–11%)
// -----------------------
function _initLaunchTracks() internal {
// multipliers are mild; randomness range is stored in upsetChanceMin/Max.
_addTrackInternal(1120, 1000, 980, 800);
upsetChanceMinBps[0] = 700;
upsetChanceMaxBps[0] = 900;
_addTrackInternal(990, 1000, 1120, 1100);
upsetChanceMinBps[1] = 1000;
upsetChanceMaxBps[1] = 1200;
_addTrackInternal(980, 1150, 1000, 1300);
upsetChanceMinBps[2] = 1200;
upsetChanceMaxBps[2] = 1500;
_addTrackInternal(1080, 980, 1080, 900);
upsetChanceMinBps[3] = 800;
upsetChanceMaxBps[3] = 1000;
_addTrackInternal(980, 990, 1140, 1000);
upsetChanceMinBps[4] = 900;
upsetChanceMaxBps[4] = 1100;
}
function _addTrackInternal(
uint16 speedMult,
uint16 accelMult,
uint16 handlingMult,
uint16
) internal {
if (trackCount >= MAX_TRACKS) revert MaxTracks();
tracks[trackCount] = Track(speedMult, accelMult, handlingMult);
trackCount++;
}
// -----------------------
// Track admin
// -----------------------
function ownerAddTrack(
uint16 speedMult,
uint16 accelMult,
uint16 handlingMult,
uint16 upsetBps_
) external onlyOwner {
_addTrackInternal(speedMult, accelMult, handlingMult, upsetBps_);
uint8 tid = trackCount - 1;
upsetChanceMinBps[tid] = upsetBps_;
upsetChanceMaxBps[tid] = upsetBps_;
}
function ownerSetTrack(
uint8 trackId,
uint16 speedMult,
uint16 accelMult,
uint16 handlingMult,
uint16 upsetBps_
) external onlyOwner {
if (trackId >= trackCount) revert InvalidTrack();
tracks[trackId] = Track(speedMult, accelMult, handlingMult);
upsetChanceMinBps[trackId] = upsetBps_;
upsetChanceMaxBps[trackId] = upsetBps_;
}
function ownerSetTrackRandomnessRange(uint8 trackId, uint16 minBps, uint16 maxBps) external onlyOwner {
if (trackId >= trackCount) revert InvalidTrack();
if (maxBps < minBps) revert InvalidRandomnessRange();
if (maxBps > MAX_BPS) revert InvalidRandomnessRange();
upsetChanceMinBps[trackId] = minBps;
upsetChanceMaxBps[trackId] = maxBps;
}
// -----------------------
// OWNER SETTERS — FULL CONTROL
// -----------------------
function transferOwnership(address newOwner) external onlyOwner {
if (newOwner == address(0)) revert NewOwnerZero();
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
function setCoreContracts(
address _carNFT,
address _upgradeRegistry,
address _upgrades1155
) external onlyOwner {
if (_carNFT == address(0) || _upgradeRegistry == address(0) || _upgrades1155 == address(0)) revert ToZero();
carNFT = IRaceCarNFT(_carNFT);
upgradeRegistry = IUpgradeRegistry(_upgradeRegistry);
upgrades1155 = IRacerUpgrades1155GameBurn(_upgrades1155);
}
function setTokens(address _pewpew, address _usdc) external onlyOwner {
if (_pewpew == address(0) || _usdc == address(0)) revert ToZero();
pewpew = IERC20(_pewpew);
usdc = IERC20(_usdc);
}
function setEconomyWiring(
address _router,
address _pewswap5
) external onlyOwner {
if (_router == address(0) || _pewswap5 == address(0)) revert ToZero();
router = IPulseXRouterV2(_router);
pewswap5 = IERC20(_pewswap5);
}
function setBenandSetLPvault(address benevolence, address lpHarvester) external onlyOwner {
if (benevolence == address(0) || lpHarvester == address(0)) revert ToZero();
benevolenceVault = benevolence;
lpHarvesterVault = lpHarvester;
}
function setVaultBlastConfig(
uint256 minHold,
uint256 amount,
uint256[5] calldata thresholds,
uint16[5] calldata bps
) external onlyOwner {
minPewpewToVaultBlast = minHold;
maxPewpewVaultSend = amount;
for (uint256 i = 0; i < 5; i++) {
if (i > 0 && thresholds[i] < thresholds[i - 1]) revert TooHigh();
if (bps[i] > MAX_BPS) revert TooHigh();
vaultBlastTierThresholds[i] = thresholds[i];
vaultBlastTierRewardBps[i] = bps[i];
}
}
function setRaceFees(
uint256 _ghost,
uint256 _drag,
uint256 _single,
uint256 _season
) external onlyOwner {
ghostRaceFee = _ghost;
dragRaceFee = _drag;
singleRaceFee = _single;
seasonStartFee = _season;
}
function setSlippageBps(uint16 bps) external onlyOwner {
if (bps > 2000) revert TooHigh();
slippageBps = bps;
}
function setStarterWinRewardBps(uint16 _dragBps, uint16 _singleBps) external onlyOwner {
if (_dragBps > MAX_BPS) revert TooHigh();
if (_singleBps > MAX_BPS) revert TooHigh();
dragStarterWinRewardBps = _dragBps;
singleStarterWinRewardBps = _singleBps;
}
function setSeasonEconomy(uint16 _burnBps, uint16 _championRewardBps) external onlyOwner {
if (_burnBps > MAX_BPS) revert TooHigh();
if (_championRewardBps > MAX_BPS) revert TooHigh();
seasonBurnBps = _burnBps;
seasonChampionRewardBps = _championRewardBps;
}
function setSeasonBlastTierRewardBps(uint16[5] calldata bps) external onlyOwner {
for (uint256 i = 0; i < 5; i++) {
if (bps[i] > MAX_BPS) revert TooHigh();
seasonBlastTierRewardBps[i] = bps[i];
}
}
function setMinPewpewToSeasonBlast(uint256 amount) external onlyOwner {
minPewpewToSeasonBlast = amount;
}
function setSeasonLegCooldownSeconds(uint32 s) external onlyOwner {
seasonLegCooldownSeconds = s;
}
function setSeasonIncludedTracks(uint8[SEASON_LEGS] calldata ids) external onlyOwner {
for (uint8 i = 0; i < SEASON_LEGS; i++) {
if (ids[i] >= trackCount) revert InvalidTrack();
seasonIncludedTracks[i] = ids[i];
}
}
function setSeasonHistoryLimit(uint256 n) external onlyOwner {
seasonHistoryLimit = n;
}
function setPaused(bool p) external onlyOwner {
paused = p;
emit PausedSet(p);
}
// -----------------------
// Unified rescue (ERC20 or native PLS)
// -----------------------
function _safeTransfer(address token, address to, uint256 amount) internal returns (bool ok) {
// IERC20.transfer selector = 0xa9059cbb
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, amount));
if (!success) return false;
if (data.length == 0) return true; // non-standard ERC20
if (data.length >= 32) return abi.decode(data, (bool));
return false;
}
function _rescue(address token, address payable to, uint256 amount) internal {
if (to == address(0)) revert ToZero();
if (token == address(0)) {
(bool ok, ) = to.call{value: amount}("");
if (!ok) revert PlsSendFailed();
} else {
if (!_safeTransfer(token, to, amount)) revert TransferFailed();
}
emit Rescue(token, to, amount);
}
function rescue(address token, address payable to, uint256 amount) external onlyOwner nonReentrant {
_rescue(token, to, amount);
}
function rescuePLS(address payable to, uint256 amount) external onlyOwner nonReentrant {
_rescue(address(0), to, amount);
}
// -----------------------
// Balance getters
// -----------------------
function getTokenBalances() external view returns (uint256 usdcBal, uint256 pewpewBal, uint256 pewswap5Bal) {
return (usdc.balanceOf(address(this)), pewpew.balanceOf(address(this)), pewswap5.balanceOf(address(this)));
}
// -----------------------
// Read-only helpers (front end)
// -----------------------
function getWiredAddresses()
external
view
returns (
address carNft,
address registry,
address upgrades,
address pewpewToken,
address usdcToken,
address pewswap5Token,
address routerAddr,
address viewsAddr,
address auxAddr,
address benevolence,
address lpHarvester
)
{
return (
address(carNFT),
address(upgradeRegistry),
address(upgrades1155),
address(pewpew),
address(usdc),
address(pewswap5),
address(router),
views,
aux,
benevolenceVault,
lpHarvesterVault
);
}
function getInternalConfig()
external
view
returns (
uint16 _slippageBps,
uint32 _seasonLegCooldownSeconds,
uint256 _seasonHistoryLimit
)
{
return (slippageBps, seasonLegCooldownSeconds, seasonHistoryLimit);
}
function getSeasonIncludedTracks() external view returns (uint8[SEASON_LEGS] memory ids) {
return seasonIncludedTracks;
}
function getSeasonLegCooldownRemaining(uint256 seasonId) external view returns (uint256 remainingSeconds) {
uint256 readyAt = seasonNextLegAvailableAt[seasonId];
return readyAt == 0 || block.timestamp >= readyAt ? 0 : readyAt - block.timestamp;
}
function getVaultBlastPolicy()
external
view
returns (
uint256[5] memory thresholds,
uint16[5] memory rewardBps,
uint256 pewpewMaxSend
)
{
thresholds = vaultBlastTierThresholds;
rewardBps = vaultBlastTierRewardBps;
pewpewMaxSend = maxPewpewVaultSend;
}
// -----------------------
// V2: PEWPEW-holder can send USDC/PEWPEW from this contract to vaults (capped)
// -----------------------
function holderSendToVault(address token, uint256 amount, bool toLpVault) external nonReentrant whenNotPaused {
uint256 callerBal = pewpew.balanceOf(msg.sender);
// Vault Blast gating is separate from season-blast tiers.
if (callerBal < minPewpewToVaultBlast) revert NeedPewpew();
address vault = toLpVault ? lpHarvesterVault : benevolenceVault;
if (vault == address(0)) revert VaultZero();
// Tier index (0..4) for 1m/5m/10m/20m/30m.
uint8 tier;
if (callerBal >= vaultBlastTierThresholds[4]) tier = 4;
else if (callerBal >= vaultBlastTierThresholds[3]) tier = 3;
else if (callerBal >= vaultBlastTierThresholds[2]) tier = 2;
else if (callerBal >= vaultBlastTierThresholds[1]) tier = 1;
else tier = 0;
// USDC send: capped to 5 USDC per call; pays the same tiered % reward system as PEWPEW.
if (token == address(usdc)) {
if (amount > 5_000_000) revert TooHigh(); // 5 USDC (6 decimals)
uint256 balBefore = IERC20(token).balanceOf(address(this));
if (!_safeTransfer(token, vault, amount)) revert TransferFailed();
uint256 usdcReward = (amount * uint256(vaultBlastTierRewardBps[tier])) / MAX_BPS;
uint256 rem = balBefore - amount;
if (usdcReward > rem) usdcReward = rem;
if (usdcReward > 0 && !_safeTransfer(token, msg.sender, usdcReward)) revert TransferFailed();
emit HolderVaultSend(msg.sender, token, vault, amount);
return;
}
// PEWPEW send: LP vault only, capped to 5,000 PEWPEW per call; pays tiered PEWPEW % reward.
if (token == address(pewpew)) {
if (!toLpVault) revert TransferFailed();
if (amount > maxPewpewVaultSend) revert TooHigh();
uint256 balBefore = pewpew.balanceOf(address(this));
if (!_safeTransfer(token, vault, amount)) revert TransferFailed();
uint256 reward = (amount * uint256(vaultBlastTierRewardBps[tier])) / MAX_BPS;
uint256 rem = balBefore - amount;
if (reward > rem) reward = rem;
if (reward > 0 && !_safeTransfer(token, msg.sender, reward)) revert TransferFailed();
emit HolderVaultSend(msg.sender, token, vault, amount);
return;
}
revert TransferFailed();
}
// -----------------------
// EQUIP / UNEQUIP LOGIC
// -----------------------
function equipUpgrade(uint256 carId, uint256[] calldata upgradeIds) external nonReentrant {
if (carNFT.ownerOf(carId) != msg.sender) revert NotCarOwner();
uint256 len = upgradeIds.length;
PendingEquipSelection[] memory selected = new PendingEquipSelection[](len);
uint256 selectedCount = 0;
for (uint256 i = 0; i < len; i++) {
uint256 upgradeId = upgradeIds[i];
if (!upgradeRegistry.exists(upgradeId)) revert InvalidUpgrade();
if (upgrades1155.balanceOf(msg.sender, upgradeId) < 1) revert NoToken();
uint8 slot = upgradeRegistry.getSlotType(upgradeId);
if (slot >= MAX_SLOTS) revert SlotTooLarge();
uint8 level = upgradeRegistry.getLevel(upgradeId);
bool handled = false;
for (uint256 j = 0; j < selectedCount; j++) {
if (selected[j].slot != slot) continue;
if (level > selected[j].level) {
selected[j].level = level;
selected[j].upgradeId = upgradeId;
}
handled = true;
break;
}
if (!handled) {
selected[selectedCount] = PendingEquipSelection({
slot: slot,
level: level,
upgradeId: upgradeId
});
selectedCount++;
}
}
for (uint256 i = 0; i < selectedCount; i++) {
uint8 slot = selected[i].slot;
uint256 upgradeId = selected[i].upgradeId;
equippedUpgradeByCarSlot[carId][slot] = upgradeId;
emit UpgradeEquipped(carId, slot, upgradeId);
}
}
function unequipUpgrade(uint256 carId, uint8[] calldata slots) external nonReentrant {
if (carNFT.ownerOf(carId) != msg.sender) revert NotCarOwner();
uint256 len = slots.length;
for (uint256 i = 0; i < len; i++) {
uint8 slot = slots[i];
if (slot >= MAX_SLOTS) revert SlotTooLarge();
bool seen = false;
for (uint256 j = 0; j < i; j++) {
if (slots[j] == slot) {
seen = true;
break;
}
}
if (seen) continue;
uint256 current = equippedUpgradeByCarSlot[carId][slot];
if (current == 0) revert NothingEquipped();
equippedUpgradeByCarSlot[carId][slot] = 0;
emit UpgradeUnequipped(carId, slot, current);
}
}
// -----------------------
// EFFECTIVE STATS
// -----------------------
function getCarEffectiveStats(uint256 carId)
public
view
returns (uint16 effSpeed, uint16 effAccel, uint16 effHandling)
{
(, , uint16 baseSpeed, uint16 baseHandling, uint16 baseAccel) = carNFT.cars(carId);
int256 speed = int256(uint256(baseSpeed));
int256 accel = int256(uint256(baseAccel));
int256 handling = int256(uint256(baseHandling));
uint8 slots = upgradeRegistry.slotCount();
if (slots > MAX_SLOTS) slots = MAX_SLOTS;
address ownerOfCar = carNFT.ownerOf(carId);
for (uint8 s = 0; s < slots; s++) {
uint256 upId = equippedUpgradeByCarSlot[carId][s];
if (upId == 0) continue;
if (upgrades1155.balanceOf(ownerOfCar, upId) == 0) continue;
(int16 ds, int16 da, int16 dh) = upgradeRegistry.getStats(upId);
speed += ds;
accel += da;
handling += dh;
}
effSpeed = _clampToUint16(speed);
effAccel = _clampToUint16(accel);
effHandling = _clampToUint16(handling);
}
function _clampToUint16(int256 v) internal pure returns (uint16) {
if (v < 0) return 0;
if (v > int256(uint256(type(uint16).max))) return type(uint16).max;
return uint16(uint256(v));
}
// -----------------------
// INTERNAL: PLS -> token swaps (via aux helper)
// -----------------------
function _swapPlsForMode(uint8 mode, uint256 plsAmount) internal returns (uint256 outReceived) {
if (plsAmount == 0) return 0;
outReceived = IGAME_AUX(_aux()).swapExactPlsForMode{value: plsAmount}(mode, address(router), slippageBps);
}
// V2: non-reverting payout for gameplay (skip if insufficient)
function _tryPayRewardPewswap5(address to, uint256 amount) internal returns (uint256 paid) {
if (amount == 0) return 0;
if (to == address(0)) revert ToZero();
uint256 bal = pewswap5.balanceOf(address(this));
if (bal < amount) {
emit RewardSkipped(to, amount);
return 0;
}
if (!_safeTransfer(address(pewswap5), to, amount)) revert RewardTransferFailed();
return amount;
}
// -----------------------
// INTERNAL: RANDOM EQUIPPED TOKEN BURN (upgrade OR visual)
// -----------------------
function _burnRandomHighestUpgrade(address player, uint256 carId) internal {
uint256 chosenId = IGAME_AUX(_aux()).pickRandomEquippedToken(player, carId);
if (chosenId != 0) {
upgrades1155.burnForGame(player, chosenId, 1);
emit UpgradeBurnedForLoss(player, carId, chosenId);
}
}
// -----------------------
// INTERNAL: DRAG TIME (NO MODIFIERS)
// -----------------------
function _computeDragTime(uint256 carId) internal view returns (uint256) {
(uint16 s, uint16 a, uint16 h) = getCarEffectiveStats(carId);
uint256 time = 200_000 - (uint256(s) * 40 + uint256(a) * 30 + uint256(h) * 10) / 100;
if (time < 10_000) time = 10_000;
return time;
}
// -----------------------
// INTERNAL: TRACK TIME (WITH MODIFIERS + UPSET)
// -----------------------
function _computeBaseTrackTime(uint256 carId, uint8 trackId)
internal
view
returns (uint256 baseTime, uint16 minBps, uint16 maxBps)
{
Track storage tr = tracks[trackId];
(uint16 s, uint16 a, uint16 h) = getCarEffectiveStats(carId);
uint256 adjS = (uint256(s) * tr.speedMult) / 1000;
uint256 adjA = (uint256(a) * tr.accelMult) / 1000;
uint256 adjH = (uint256(h) * tr.handlingMult) / 1000;
baseTime = 250_000 - (adjS * 30 + adjA * 40 + adjH * 30) / 100;
if (baseTime < 15_000) baseTime = 15_000;
minBps = upsetChanceMinBps[trackId];
maxBps = upsetChanceMaxBps[trackId];
}
function _applyUpset(uint256 baseTime, uint16 minBps, uint16 maxBps, uint256 seed)
internal
pure
returns (uint256)
{
if (maxBps == 0) return baseTime;
uint16 chance;
if (maxBps == minBps) {
chance = minBps;
} else {
chance = uint16(minBps + (seed % (uint256(maxBps - minBps) + 1)));
}
uint256 roll = uint256(keccak256(abi.encodePacked(seed, uint256(1)))) % 10_000;
if (roll < chance) {
return (baseTime * 110) / 100;
}
return baseTime;
}
function _computeTrackTimeFromEntropy(uint256 carId, uint8 trackId, bytes32 entropyRoot)
internal
view
returns (uint256)
{
if (trackId >= trackCount) revert InvalidTrack();
(uint256 baseTime, uint16 minBps, uint16 maxBps) = _computeBaseTrackTime(carId, trackId);
if (maxBps == 0) return baseTime;
uint256 seed = uint256(
keccak256(abi.encodePacked(entropyRoot, carId, trackId, baseTime))
);
return _applyUpset(baseTime, minBps, maxBps, seed);
}
function _computeTrackTime(uint256 carId, uint8 trackId) internal view returns (uint256) {
return _computeTrackTimeFromEntropy(carId, trackId, blockhash(block.number - 1));
}
function _computeSeasonTrackTime(
uint256 carId,
uint8 trackId,
bytes32 entropyRoot
) internal view returns (uint256) {
return _computeTrackTimeFromEntropy(carId, trackId, entropyRoot);
}
function _nextSeasonEntropy(uint256 seasonId, uint8 leg, address raceCaller)
internal
returns (bytes32 entropyRoot)
{
uint256 raceNonce = seasonRaceNonce[seasonId] + 1;
seasonRaceNonce[seasonId] = raceNonce;
bytes32 delayedBlockHash = block.number > 5 ? blockhash(block.number - 5) : blockhash(block.number - 1);
entropyRoot = keccak256(
abi.encodePacked(
seasonEntropyAccumulator,
block.prevrandao,
delayedBlockHash,
seasonId,
leg,
raceCaller,
raceNonce
)
);
seasonEntropyAccumulator = uint256(entropyRoot);
}
// -----------------------
// DRAG RACE (2 CARS)
// Winner always declared; reward (if configured) goes to the winner owner.
// Existing burn-on-loss for starter remains.
// -----------------------
function dragRace(uint256 car1, uint256 car2) external payable nonReentrant whenNotPaused {
if (car1 == car2) revert SameCar();
if (carNFT.ownerOf(car1) != msg.sender) revert StarterMustOwnCar1();
if (msg.value != dragRaceFee) revert WrongPlsAmount();
uint256 pewswap5Received = _swapPlsForMode(2, dragRaceFee);
uint256 t1 = _computeDragTime(car1);
uint256 t2 = _computeDragTime(car2);
if (bestDragTimeMsByCar[car1] == 0 || t1 < bestDragTimeMsByCar[car1]) {
bestDragTimeMsByCar[car1] = t1;
}
if (bestDragTimeMsByCar[car2] == 0 || t2 < bestDragTimeMsByCar[car2]) {
bestDragTimeMsByCar[car2] = t2;
}
bool starterWon = t1 <= t2;
uint256 winnerCarId = starterWon ? car1 : car2;
address winnerOwner = carNFT.ownerOf(winnerCarId);
if (!starterWon) {
_burnRandomHighestUpgrade(msg.sender, car1);
}
uint256 rewardPaid = 0;
if (starterWon) {
uint256 payout = (pewswap5Received * uint256(dragStarterWinRewardBps)) / MAX_BPS;
rewardPaid = _tryPayRewardPewswap5(msg.sender, payout);
}
emit DragRaceResult(car1, t1, car2, t2, msg.sender, starterWon);
uint256 rid = nextDragRaceId++;
IGAME_AUX(_aux()).storeDragRaceReplay(
rid,
car1,
car2,
t1,
t2,
winnerCarId,
winnerOwner,
msg.sender,
uint64(block.timestamp),
rewardPaid
);
emit DragRaceReplayStored(rid, winnerCarId, winnerOwner, rewardPaid);
}
// -----------------------
// GHOST DRAG RACE (FREE)
// -----------------------
function ghostDragRace(uint256 carId) external payable nonReentrant whenNotPaused {
if (carNFT.ownerOf(carId) != msg.sender) revert NotCarOwner();
if (msg.value != ghostRaceFee) revert WrongPlsAmount();
_swapPlsForMode(1, ghostRaceFee);
uint256 newTime = _computeDragTime(carId);
uint256 prevBest = bestDragTimeMsByCar[carId];
if (prevBest == 0 || newTime < prevBest) {
bestDragTimeMsByCar[carId] = newTime;
}
emit GhostRaceResult(carId, newTime, prevBest);
}
// -----------------------
// SINGLE RACE (V2, up to 32 cars)
// - starter must own carIds[0] to prevent arbitrary third-party race submissions
// - winner is lowest computed time
// - reward (if any) goes to winner owner
// - results stored for replay
// -----------------------
function singleRaceBatch(uint256[] calldata carIds, uint8 trackId)
external
payable
nonReentrant
whenNotPaused
returns (uint256 raceId, uint256 winnerCarId, uint256 winnerTime)
{
if (carIds.length < 2) revert Need2Cars();
if (carIds.length > MAX_SINGLE_RACE_CARS) revert TooManyCars();
if (carNFT.ownerOf(carIds[0]) != msg.sender) revert StarterMustOwnFirstCar();
if (trackId >= trackCount) revert InvalidTrack();
IGAME_AUX(_aux()).validateNoZeroNoDups(carIds);
uint256[] memory cars = new uint256[](carIds.length);
for (uint256 i = 0; i < carIds.length; i++) {
cars[i] = carIds[i];
}
(raceId, winnerCarId, winnerTime,,) = _singleRaceBatch(cars, trackId);
}
function _singleRaceBatch(uint256[] memory carIds, uint8 trackId)
internal
returns (uint256 raceId, uint256 winnerCarId, uint256 winnerTime, address winnerOwner, uint256 rewardPaid)
{
if (msg.value != singleRaceFee) revert WrongPlsAmount();
uint256 pewswap5Received = _swapPlsForMode(3, singleRaceFee);
raceId = nextSingleRaceBatchId++;
winnerTime = type(uint256).max;
winnerCarId = 0;
uint256[] memory times = new uint256[](carIds.length);
for (uint256 i2 = 0; i2 < carIds.length; i2++) {
uint256 cid = carIds[i2];
uint256 t = _computeTrackTime(cid, trackId);
uint256 prev = bestTrackTimeMsByCarTrack[cid][trackId];
if (prev == 0 || t < prev) {
bestTrackTimeMsByCarTrack[cid][trackId] = t;
}
times[i2] = t;
if (t < winnerTime) {
winnerTime = t;
winnerCarId = cid;
}
}
winnerOwner = carNFT.ownerOf(winnerCarId);
// Burn 1 upgrade if the starter (carIds[0]) loses.
if (carIds[0] != winnerCarId) {
_burnRandomHighestUpgrade(msg.sender, carIds[0]);
rewardPaid = 0;
} else {
uint256 payout = (pewswap5Received * uint256(singleStarterWinRewardBps)) / MAX_BPS;
rewardPaid = _tryPayRewardPewswap5(msg.sender, payout);
}
// No payout to non-starter winners.
IGAME_AUX(_aux()).storeSingleRaceBatchReplay(
raceId,
trackId,
carIds,
times,
winnerCarId,
winnerOwner,
winnerTime,
msg.sender,
uint64(block.timestamp),
rewardPaid
);
emit SingleRaceBatchStored(raceId, trackId, winnerCarId, winnerOwner, winnerTime, rewardPaid);
}
// -----------------------
// SEASONS (AGGREGATE, 5 LEGS, FIXED ROSTER)
// -----------------------
function getSeasonCars(uint256 seasonId) external view returns (uint256[] memory) {
return _seasonCars[seasonId];
}
function startSeason(uint256 starterCarId, uint256[] calldata carIds)
external
payable
nonReentrant
whenNotPaused
returns (uint256 seasonId)
{
if (activeSeasons.length >= 5) revert SeasonLimit();
if (carIds.length < 2) revert Need2Cars();
if (carIds.length > MAX_RACE_CARS) revert TooManyCars();
if (carIds[0] != starterCarId) revert StarterCarMismatch();
if (carNFT.ownerOf(starterCarId) != msg.sender) revert NotCarOwner();
if (msg.value != seasonStartFee) revert WrongPlsAmount();
IGAME_AUX(_aux()).validateNoZeroNoDups(carIds);
uint256 pewpewReceived = _swapPlsForMode(4, seasonStartFee);
uint256 burnAmount = (pewpewReceived * uint256(seasonBurnBps)) / MAX_BPS;
if (burnAmount > pewpewReceived) revert BurnGreaterThanReceived();
if (burnAmount > 0) IERC20Burnable(address(pewpew)).burn(burnAmount);
seasonId = nextSeasonId++;
activeSeasons.push(seasonId);
isSeasonActive[seasonId] = true;
seasonRaceCount[seasonId] = 0;
seasonNextLegAvailableAt[seasonId] = 0;
for (uint256 k = 0; k < carIds.length; k++) {
_seasonCars[seasonId].push(carIds[k]);
}
emit SeasonStarted(
seasonId,
msg.sender,
starterCarId,
carIds.length,
pewpewReceived,
burnAmount
);
}
function endSeason(uint256 seasonId) external onlyOwner {
if (!isSeasonActive[seasonId]) revert NotActive();
_endSeasonInternal(seasonId);
_pushSeasonHistoryAndMaybePurge(seasonId);
}
function _endSeasonInternal(uint256 seasonId) internal {
isSeasonActive[seasonId] = false;
uint256 len = activeSeasons.length;
for (uint256 i = 0; i < len; i++) {
if (activeSeasons[i] == seasonId) {
activeSeasons[i] = activeSeasons[len - 1];
activeSeasons.pop();
break;
}
}
}
// Leg 1: run by anyone
function seasonRace(uint256 seasonId) external nonReentrant whenNotPaused {
if (!isSeasonActive[seasonId]) revert SeasonInactive();
uint8 leg = seasonRaceCount[seasonId] + 1;
if (leg != 1) revert SeasonLegCooldown();
_runSeasonLeg(seasonId, leg, false);
}
// Legs 2..5: run via blast; caller must hold enough PEWPEW and respects cooldown
function blastSeasonRace(uint256 seasonId) external nonReentrant whenNotPaused {
if (!isSeasonActive[seasonId]) revert SeasonInactive();
if (pewpew.balanceOf(msg.sender) < minPewpewToSeasonBlast) revert Need1MPewpew();
uint8 leg = seasonRaceCount[seasonId] + 1;
if (leg < 2 || leg > SEASON_LEGS) revert SeasonComplete();
uint256 readyAt = seasonNextLegAvailableAt[seasonId];
if (readyAt != 0 && block.timestamp < readyAt) revert SeasonLegCooldown();
_runSeasonLeg(seasonId, leg, true);
}
function _runSeasonLeg(
uint256 seasonId,
uint8 leg,
bool payBlastReward
) internal {
uint8 trackId = seasonIncludedTracks[leg - 1];
if (trackId >= trackCount) revert InvalidTrack();
bytes32 entropyRoot = _nextSeasonEntropy(seasonId, leg, msg.sender);
(uint256 bestLegTime, uint256 winnerLegCarId) = _seasonLegComputeAndUpdate(
seasonId,
trackId,
entropyRoot
);
uint256 callerReward = 0;
if (payBlastReward) {
uint16 tierBps;
uint256 callerBal = pewpew.balanceOf(msg.sender);
if (callerBal >= SEASON_BLAST_TIER5) tierBps = seasonBlastTierRewardBps[4];
else if (callerBal >= SEASON_BLAST_TIER4) tierBps = seasonBlastTierRewardBps[3];
else if (callerBal >= SEASON_BLAST_TIER3) tierBps = seasonBlastTierRewardBps[2];
else if (callerBal >= SEASON_BLAST_TIER2) tierBps = seasonBlastTierRewardBps[1];
else tierBps = seasonBlastTierRewardBps[0];
callerReward = (pewpew.balanceOf(address(this)) * uint256(tierBps)) / MAX_BPS;
if (callerReward > 0) {
uint256 bal = pewpew.balanceOf(address(this));
if (callerReward > bal) callerReward = bal;
if (callerReward > 0) {
if (!_safeTransfer(address(pewpew), msg.sender, callerReward)) revert CallerPewpewTransferFailed();
}
}
}
seasonRaceCount[seasonId] = leg;
IGAME_AUX(_aux()).storeSeasonLeg(seasonId, leg, trackId, winnerLegCarId, bestLegTime, block.timestamp);
emit SeasonRaceLegResult(seasonId, leg, trackId, msg.sender, callerReward, winnerLegCarId, bestLegTime);
if (leg < SEASON_LEGS) {
seasonNextLegAvailableAt[seasonId] = block.timestamp + seasonLegCooldownSeconds;
}
if (leg == SEASON_LEGS) {
(uint256 winnerOverallCarId, address winnerOwner, uint256 prizePaid) = _finalizeSeasonAndTryPay(seasonId);
emit SeasonFinalized(seasonId, trackId, winnerOverallCarId, winnerOwner, prizePaid);
_endSeasonInternal(seasonId);
_pushSeasonHistoryAndMaybePurge(seasonId);
}
}
function _seasonLegComputeAndUpdate(
uint256 seasonId,
uint8 trackId,
bytes32 entropyRoot
) internal returns (uint256 bestLegTime, uint256 winnerLegCarId) {
uint256[] storage carIds = _seasonCars[seasonId];
if (carIds.length < 2) revert NoRoster();
bestLegTime = type(uint256).max;
for (uint256 i = 0; i < carIds.length; i++) {
uint256 cid = carIds[i];
uint256 t = _computeSeasonTrackTime(cid, trackId, entropyRoot);
uint256 bestGlobal = bestTrackTimeMsByCarTrack[cid][trackId];
if (bestGlobal == 0 || t < bestGlobal) {
bestTrackTimeMsByCarTrack[cid][trackId] = t;
}
seasonTotalTimeMs[seasonId][cid] += t;
if (t < bestLegTime) {
bestLegTime = t;
winnerLegCarId = cid;
}
}
}
function _finalizeSeasonAndTryPay(uint256 seasonId)
internal
returns (uint256 winnerCarIdOverall, address winnerOwner, uint256 prizePaid)
{
uint256[] storage roster = _seasonCars[seasonId];
uint256 n = roster.length;
uint256 bestTotal = type(uint256).max;
uint256 bestCar = 0;
address bestOwner = address(0);
for (uint256 i = 0; i < n; i++) {
uint256 cid = roster[i];
address o = carNFT.ownerOf(cid);
uint256 total = seasonTotalTimeMs[seasonId][cid];
if (total < bestTotal) {
bestTotal = total;
bestCar = cid;
bestOwner = o;
}
}
winnerCarIdOverall = bestCar;
winnerOwner = bestOwner;
prizePaid = 0;
if (winnerCarIdOverall != 0) {
uint256 payout = (pewswap5.balanceOf(address(this)) * uint256(seasonChampionRewardBps)) / MAX_BPS;
prizePaid = _tryPayRewardPewswap5(winnerOwner, payout);
}
IGAME_AUX(_aux()).storeSeasonFinal(seasonId, winnerCarIdOverall, winnerOwner, prizePaid);
// No burn-on-loss for season starter (only drag/single modes burn).
}
function _pushSeasonHistoryAndMaybePurge(uint256 seasonId) internal {
seasonHistory.push(seasonId);
uint256 lim = seasonHistoryLimit;
if (lim == 0) return;
if (seasonHistory.length <= lim) return;
uint256 oldId = seasonHistory[0];
// shift left (small array; lim default 30)
for (uint256 i = 0; i + 1 < seasonHistory.length; i++) {
seasonHistory[i] = seasonHistory[i + 1];
}
seasonHistory.pop();
_purgeSeasonData(oldId);
}
function _purgeSeasonData(uint256 seasonId) internal {
uint256[] storage roster = _seasonCars[seasonId];
uint256 n = roster.length;
for (uint256 i = 0; i < n; i++) {
uint256 cid = roster[i];
delete seasonTotalTimeMs[seasonId][cid];
}
// clear roster
delete _seasonCars[seasonId];
delete seasonRaceCount[seasonId];
delete seasonNextLegAvailableAt[seasonId];
delete seasonRaceNonce[seasonId];
IGAME_AUX(_aux()).purgeSeason(seasonId);
}
// -----------------------
// Visual manifests (SIDE/TOP)
// Implemented via an external views helper to keep this contract under the 24KB limit.
// -----------------------
function _getViewVisuals(bytes4 selector, uint256 carId)
internal
view
returns (uint32 modelId, uint256 paintUpgradeId, uint256[] memory decalUpgradeIds, bool hasSupercharger)
{
address vc = views;
if (vc == address(0)) revert HelperNotSet();
(bool ok, bytes memory data) = vc.staticcall(
abi.encodeWithSelector(selector, address(this), carId)
);
if (!ok) revert ViewsCallFailed();
return abi.decode(data, (uint32, uint256, uint256[], bool));
}
function getSideViewVisuals(uint256 carId) external view returns (SideViewVisuals memory v) {
(uint32 modelId, uint256 paintUpgradeId, uint256[] memory decalUpgradeIds, bool hasSupercharger) =
_getViewVisuals(_SEL_GET_SIDE_VISUALS, carId);
v.modelId = modelId;
v.paintUpgradeId = paintUpgradeId;
v.decalUpgradeIds = decalUpgradeIds;
v.hasSupercharger = hasSupercharger;
}
function getTopViewVisuals(uint256 carId) external view returns (TopViewVisuals memory v) {
(uint32 modelId, uint256 paintUpgradeId, uint256[] memory decalUpgradeIds, bool hasSupercharger) =
_getViewVisuals(_SEL_GET_TOP_VISUALS, carId);
v.modelId = modelId;
v.paintUpgradeId = paintUpgradeId;
v.decalUpgradeIds = decalUpgradeIds;
v.hasSupercharger = hasSupercharger;
}
}