HoodFrens

0xacc9b7c268273b6f4483883d3bd5550fc26d2a5d

Verification
Verified
v0.8.20+commit.a1b79de6
Type
Contract
35,225 bytes
ABI entries
153
66 read · 43 write
License
mit

Contract information

Address
0xacc9b7c268273b6f4483883d3bd5550fc26d2a5d
Chain
Robinhood Chain (4663)
Compiler
v0.8.20+commit.a1b79de6
Optimization
Enabled
Creation tx
0x2bdd421e7a…e7159bbce3

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (66)

BONDING_CURVE_DENOMINATOR()uint256
BPS()uint256
COMMISSION_BPS()uint256
DEFAULT_ADMIN_ROLE()bytes32
IPO_FEE_DURATION()uint256
IPO_LOTS_PER_CALL_MAX()uint256
IPO_LOTS_PER_CALL_MIN()uint256
IPO_MAX_LOTS_PER_WALLET()uint256
IPO_PRIZE_FEE_BPS()uint256
IPO_PROTOCOL_FEE_BPS()uint256
IPO_TRADES_PER_SECOND()uint256
LOTS_PER_CALL_MAX()uint256
LOTS_PER_CALL_MIN()uint256
LOT_SIZE_UNITS()uint256
MAX_IPO_FEE_DURATION()uint256
MAX_IPO_PRIZE_FEE_BPS()uint256
MAX_IPO_PROTOCOL_FEE_BPS()uint256
MAX_PROTOCOL_FEE_BPS()uint256
MAX_SELL_PRIZE_FEE_BPS()uint256
MAX_TOTAL_COMMISSION_BPS()uint256
MAX_TRANSFER_GAS()uint256
MIN_IPO_FEE_DURATION()uint256
MIN_TRANSFER_GAS()uint256
ONE_FULL_SHARE_IN_UNITS()uint256
PROTOCOL_FEE_GROSS_BPS()uint256
REFERRAL_FEE_BPS()uint256
ROLE_CARD_PRIZE_MANAGER()bytes32
ROLE_ETH_PRIZE_MANAGER()bytes32
ROLE_REFERRER_MANAGER()bytes32
ROLE_STORAGE_CLEANER()bytes32
ROLE_TRADING_TOGGLER()bytes32
SELL_PRIZE_FEE_BPS()uint256
SHARES_DECIMALS()uint8
calculateGrossSellPayoutByLots(uint256, uint256)uint256
calculateGrossSellPayoutByUnits(uint256, uint256)uint256
customGasLimit(address)uint256
defaultTransferGasLimit()uint256
getAllPlayers(uint256, uint256)uint256[], string[], bool[]
getBuyPriceByLots(uint256, uint256)uint256
getBuyPriceByUnits(uint256, uint256)uint256
getBuyPriceRaw(uint256, uint256)uint256
getEffectiveGasLimit(address)uint256, bool
getFractionalRemainder(address, uint256)uint256, uint256, uint256
getIpoPrizeFeeEndTime(uint256)uint256
getPortfolioFractionalRemainders(address, uint256, uint256)uint256[], uint256[]
getPortfolioHoldingsInLots(address, uint256, uint256)uint256[], uint256[]
getPortfolioHoldingsInUnits(address, uint256, uint256)uint256[], uint256[]
getRoleAdmin(bytes32)bytes32
getUserPlayerHoldingInFullShares(address, uint256)uint256
getUserPlayerHoldingInLots(address, uint256)uint256
getUserPlayerHoldingInUnits(address, uint256)uint256
hasRole(bytes32, address)bool
isIpoPrizeFeeActive(uint256)bool
nextPlayerId()uint256
owner()address
pauseReason()string
paused()bool
pendingOwner()address
players(uint256)string, bool, uint256, uint256
prizeWallet()address
protocolWallet()address
referrerOf(address)address
sharesBalance(uint256, address)uint256
sharesSupply(uint256)uint256
supportsInterface(bytes4)bool
totalEthPrizesAwarded()uint256

Events (36)

BatchCleanupCompletedContractPausedContractUnpausedCustomGasLimitRemovedCustomGasLimitSetDefaultTransferGasLimitUpdatedEthPrizeAwardedEthPrizeDepositedFeesUpdatedIpoBuyFeesAppliedIpoParametersUpdatedIpoTrackingCleanedOwnershipTransferStartedOwnershipTransferredPausedPlayerAddedPlayerIPOPlayerSharesSupplyChangedPrizeTransferFailedReferralFeePaidReferralFeeRedirectedToProtocolReferrerMigratedReferrerSetRoleAdminChangedRoleGrantedRoleRevokedSharePrizeAwardedSharePrizeDepositedSharesTransferredTradeTradingParametersUpdatedTradingStatusUpdatedUnpausedUserSharesChangedWalletsUpdatedZeroBalanceCleaned

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_prizeWallet",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_protocolWallet",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AccessControlBadConfirmation",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "neededRole",
        "type": "bytes32"
      }
    ],
    "name": "AccessControlUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EnforcedPause",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExpectedPause",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "string",
        "name": "cleanupType",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "processed",
        "type": "uint256"
      }
    ],
    "name": "BatchCleanupCompleted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "string",
        "name": "reason",
        "type": "string"
      }
    ],
    "name": "ContractPaused",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [],
    "name": "ContractUnpaused",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "CustomGasLimitRemoved",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "gasLimit",
        "type": "uint256"
      }
    ],
    "name": "CustomGasLimitSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "oldLimit",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newLimit",
        "type": "uint256"
      }
    ],
    "name": "DefaultTransferGasLimitUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "winner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amountInWei",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "description",
        "type": "string"
      }
    ],
    "name": "EthPrizeAwarded",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amountInWei",
        "type": "uint256"
      }
    ],
    "name": "EthPrizeDeposited",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newSellPrizeFeeBps",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newProtocolFeeBps",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newIpoPrizeFeeBps",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newIpoProtocolFeeBps",
        "type": "uint256"
      }
    ],
    "name": "FeesUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "baseCostInWei",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "prizeFeeInWei",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "protocolFeeInWei",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "totalCostInWei",
        "type": "uint256"
      }
    ],
    "name": "IpoBuyFeesApplied",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newIpoFeeDuration",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newIpoLotsPerCallMin",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newIpoLotsPerCallMax",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newIpoMaxLotsPerWallet",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newIpoTradesPerSecond",
        "type": "uint256"
      }
    ],
    "name": "IpoParametersUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "lotsUsed",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tradesUsed",
        "type": "uint256"
      }
    ],
    "name": "IpoTrackingCleaned",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Paused",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "snipeAmountInUnits",
        "type": "uint256"
      }
    ],
    "name": "PlayerAdded",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "startTimestamp",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "endTimestamp",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "currentSupply",
        "type": "uint256"
      }
    ],
    "name": "PlayerIPO",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "int256",
        "name": "supplyChangeInUnits",
        "type": "int256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newTotalSupplyInUnits",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "currentPriceInWei",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "marketCapInWei",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "reason",
        "type": "string"
      }
    ],
    "name": "PlayerSharesSupplyChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "intendedRecipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "description",
        "type": "string"
      }
    ],
    "name": "PrizeTransferFailed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "referrer",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amountInWei",
        "type": "uint256"
      }
    ],
    "name": "ReferralFeePaid",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "intendedRecipient",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "seller",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "ReferralFeeRedirectedToProtocol",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldReferrer",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newReferrer",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "referralCount",
        "type": "uint256"
      }
    ],
    "name": "ReferrerMigrated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "referrer",
        "type": "address"
      }
    ],
    "name": "ReferrerSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "previousAdminRole",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "newAdminRole",
        "type": "bytes32"
      }
    ],
    "name": "RoleAdminChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "RoleGranted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "RoleRevoked",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "winner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amountInUnits",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "description",
        "type": "string"
      }
    ],
    "name": "SharePrizeAwarded",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amountInUnits",
        "type": "uint256"
      }
    ],
    "name": "SharePrizeDeposited",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amountInUnits",
        "type": "uint256"
      }
    ],
    "name": "SharesTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "trader",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "isBuy",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amountInUnits",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "priceInWei",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "feeInWei",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newSupplyInUnits",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "isIPOWindow",
        "type": "bool"
      }
    ],
    "name": "Trade",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newLotSizeUnits",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newLotsPerCallMin",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newLotsPerCallMax",
        "type": "uint256"
      }
    ],
    "name": "TradingParametersUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "enabled",
        "type": "bool"
      }
    ],
    "name": "TradingStatusUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Unpaused",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "int256",
        "name": "amountChangeInUnits",
        "type": "int256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newUserBalanceInUnits",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "totalCostInWei",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "totalFeesInWei",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "netAmountInWei",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "reason",
        "type": "string"
      }
    ],
    "name": "UserSharesChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "newPrizeWallet",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "newProtocolWallet",
        "type": "address"
      }
    ],
    "name": "WalletsUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "ZeroBalanceCleaned",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BONDING_CURVE_DENOMINATOR",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "COMMISSION_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "DEFAULT_ADMIN_ROLE",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "IPO_FEE_DURATION",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "IPO_LOTS_PER_CALL_MAX",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "IPO_LOTS_PER_CALL_MIN",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "IPO_MAX_LOTS_PER_WALLET",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "IPO_PRIZE_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "IPO_PROTOCOL_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "IPO_TRADES_PER_SECOND",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "LOTS_PER_CALL_MAX",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "LOTS_PER_CALL_MIN",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "LOT_SIZE_UNITS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_IPO_FEE_DURATION",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_IPO_PRIZE_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_IPO_PROTOCOL_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_PROTOCOL_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_SELL_PRIZE_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_TOTAL_COMMISSION_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_TRANSFER_GAS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_IPO_FEE_DURATION",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_TRANSFER_GAS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ONE_FULL_SHARE_IN_UNITS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PROTOCOL_FEE_GROSS_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "REFERRAL_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ROLE_CARD_PRIZE_MANAGER",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ROLE_ETH_PRIZE_MANAGER",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ROLE_REFERRER_MANAGER",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ROLE_STORAGE_CLEANER",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ROLE_TRADING_TOGGLER",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SELL_PRIZE_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SHARES_DECIMALS",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "numLots",
        "type": "uint256"
      }
    ],
    "name": "addPlayerAndSnipeByLots",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "snipeAmountUnits",
        "type": "uint256"
      }
    ],
    "name": "addPlayerAndSnipeByUnits",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "winner",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "string",
        "name": "description",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      }
    ],
    "name": "awardEthPrize",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "winner",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "string",
        "name": "description",
        "type": "string"
      }
    ],
    "name": "awardEthPrize",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address[]",
        "name": "winners",
        "type": "address[]"
      },
      {
        "internalType": "uint256[]",
        "name": "amounts",
        "type": "uint256[]"
      },
      {
        "internalType": "string[]",
        "name": "descriptions",
        "type": "string[]"
      },
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      }
    ],
    "name": "awardEthPrizeBatch",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address[]",
        "name": "winners",
        "type": "address[]"
      },
      {
        "internalType": "uint256[]",
        "name": "amounts",
        "type": "uint256[]"
      },
      {
        "internalType": "string[]",
        "name": "descriptions",
        "type": "string[]"
      }
    ],
    "name": "awardEthPrizeBatch",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256[]",
        "name": "playerIds",
        "type": "uint256[]"
      },
      {
        "internalType": "address[]",
        "name": "winners",
        "type": "address[]"
      },
      {
        "internalType": "uint256[]",
        "name": "amountUnits",
        "type": "uint256[]"
      },
      {
        "internalType": "string[]",
        "name": "descriptions",
        "type": "string[]"
      }
    ],
    "name": "awardSharePrizeBatchMultiPlayerMultiUser",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256[]",
        "name": "playerIds",
        "type": "uint256[]"
      },
      {
        "internalType": "address[]",
        "name": "winners",
        "type": "address[]"
      },
      {
        "internalType": "uint256[]",
        "name": "numLots",
        "type": "uint256[]"
      },
      {
        "internalType": "string[]",
        "name": "descriptions",
        "type": "string[]"
      }
    ],
    "name": "awardSharePrizeBatchMultiPlayerMultiUserByLots",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "winner",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "numLots",
        "type": "uint256"
      },
      {
        "internalType": "string",
        "name": "description",
        "type": "string"
      }
    ],
    "name": "awardSharePrizeByLots",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "playerId",
            "type": "uint256"
          },
          {
            "internalType": "address",
            "name": "user",
            "type": "address"
          }
        ],
        "internalType": "struct HoodFrens.CleanupTarget[]",
        "name": "targets",
        "type": "tuple[]"
      }
    ],
    "name": "batchCleanupIpoTracking",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "processed",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "skippedInvalidPlayer",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "skippedActiveIpo",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "skippedAlreadyClean",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "playerId",
            "type": "uint256"
          },
          {
            "internalType": "address",
            "name": "user",
            "type": "address"
          }
        ],
        "internalType": "struct HoodFrens.CleanupTarget[]",
        "name": "targets",
        "type": "tuple[]"
      }
    ],
    "name": "batchCleanupZeroBalances",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "processed",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "skippedInvalidPlayer",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "skippedNonZeroBalance",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "numLots",
        "type": "uint256"
      }
    ],
    "name": "buySharesByLots",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amountUnits",
        "type": "uint256"
      }
    ],
    "name": "buySharesByUnits",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "numLots",
        "type": "uint256"
      }
    ],
    "name": "calculateGrossSellPayoutByLots",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "a",
        "type": "uint256"
      }
    ],
    "name": "calculateGrossSellPayoutByUnits",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "customGasLimit",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "defaultTransferGasLimit",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "offset",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "limit",
        "type": "uint256"
      }
    ],
    "name": "getAllPlayers",
    "outputs": [
      {
        "internalType": "uint256[]",
        "name": "ids",
        "type": "uint256[]"
      },
      {
        "internalType": "string[]",
        "name": "names",
        "type": "string[]"
      },
      {
        "internalType": "bool[]",
        "name": "tradingEnableds",
        "type": "bool[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "numLots",
        "type": "uint256"
      }
    ],
    "name": "getBuyPriceByLots",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amountUnits",
        "type": "uint256"
      }
    ],
    "name": "getBuyPriceByUnits",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "a",
        "type": "uint256"
      }
    ],
    "name": "getBuyPriceRaw",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "getEffectiveGasLimit",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "gasLimit",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "isCustom",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      }
    ],
    "name": "getFractionalRemainder",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "remainder",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "estimatedGrossPayout",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "estimatedNetPayout",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      }
    ],
    "name": "getIpoPrizeFeeEndTime",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "offset",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "limit",
        "type": "uint256"
      }
    ],
    "name": "getPortfolioFractionalRemainders",
    "outputs": [
      {
        "internalType": "uint256[]",
        "name": "playerIds",
        "type": "uint256[]"
      },
      {
        "internalType": "uint256[]",
        "name": "remainders",
        "type": "uint256[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "offset",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "limit",
        "type": "uint256"
      }
    ],
    "name": "getPortfolioHoldingsInLots",
    "outputs": [
      {
        "internalType": "uint256[]",
        "name": "playerIds",
        "type": "uint256[]"
      },
      {
        "internalType": "uint256[]",
        "name": "quantities",
        "type": "uint256[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "offset",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "limit",
        "type": "uint256"
      }
    ],
    "name": "getPortfolioHoldingsInUnits",
    "outputs": [
      {
        "internalType": "uint256[]",
        "name": "playerIds",
        "type": "uint256[]"
      },
      {
        "internalType": "uint256[]",
        "name": "quantities",
        "type": "uint256[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      }
    ],
    "name": "getRoleAdmin",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      }
    ],
    "name": "getUserPlayerHoldingInFullShares",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      }
    ],
    "name": "getUserPlayerHoldingInLots",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      }
    ],
    "name": "getUserPlayerHoldingInUnits",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "grantRole",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "hasRole",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      }
    ],
    "name": "isIpoPrizeFeeActive",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "nextPlayerId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "reason",
        "type": "string"
      }
    ],
    "name": "pause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pauseReason",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "paused",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "players",
    "outputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "bool",
        "name": "tradingEnabled",
        "type": "bool"
      },
      {
        "internalType": "uint256",
        "name": "ipoWindowStartTimestamp",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "ipoWindowEndTimestamp",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "prizeWallet",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "protocolWallet",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "winner",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "string",
        "name": "description",
        "type": "string"
      }
    ],
    "name": "recordEthPrizeAwarded",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "winner",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "string",
        "name": "description",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      }
    ],
    "name": "recordEthPrizeAwarded",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "referrerOf",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "callerConfirmation",
        "type": "address"
      }
    ],
    "name": "renounceRole",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "role",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "revokeRole",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minNetPayout",
        "type": "uint256"
      }
    ],
    "name": "sellFractionalRemainder",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "numLots",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minNetPayout",
        "type": "uint256"
      }
    ],
    "name": "sellSharesByLots",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amountUnits",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minNetPayout",
        "type": "uint256"
      }
    ],
    "name": "sellSharesByUnits",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "gasLimit",
        "type": "uint256"
      }
    ],
    "name": "setCustomGasLimit",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address[]",
        "name": "recipients",
        "type": "address[]"
      },
      {
        "internalType": "uint256[]",
        "name": "gasLimits",
        "type": "uint256[]"
      }
    ],
    "name": "setCustomGasLimitBatch",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "newLimit",
        "type": "uint256"
      }
    ],
    "name": "setDefaultTransferGasLimit",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "_sellPrizeFeeBps",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "_protocolFeeGrossBps",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "_ipoPrizeFeeBps",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "_ipoProtocolFeeBps",
        "type": "uint256"
      }
    ],
    "name": "setFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "_ipoFeeDuration",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "_ipoLotsPerCallMin",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "_ipoLotsPerCallMax",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "_ipoMaxLotsPerWallet",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "_ipoTradesPerSecond",
        "type": "uint256"
      }
    ],
    "name": "setIpoParameters",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address[]",
        "name": "users",
        "type": "address[]"
      },
      {
        "internalType": "address[]",
        "name": "referrers",
        "type": "address[]"
      }
    ],
    "name": "setMultipleUserReferrers",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_prize",
        "type": "address"
      }
    ],
    "name": "setPrizeWallet",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_protocol",
        "type": "address"
      }
    ],
    "name": "setProtocolWallet",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "_lotSizeUnits",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "_lotsPerCallMin",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "_lotsPerCallMax",
        "type": "uint256"
      }
    ],
    "name": "setTradingParameters",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "enabled",
        "type": "bool"
      }
    ],
    "name": "setTradingStatus",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256[]",
        "name": "playerIds",
        "type": "uint256[]"
      },
      {
        "internalType": "bool",
        "name": "enabled",
        "type": "bool"
      }
    ],
    "name": "setTradingStatusBulk",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "updatedCount",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "referrer",
        "type": "address"
      }
    ],
    "name": "setUserReferrer",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "sharesBalance",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "sharesSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes4",
        "name": "interfaceId",
        "type": "bytes4"
      }
    ],
    "name": "supportsInterface",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalEthPrizesAwarded",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "transferAllSharesInWallet",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      }
    ],
    "name": "transferAllSharesOfIndividualPlayer",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "numLots",
        "type": "uint256"
      }
    ],
    "name": "transferSharesByLots",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "playerId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amountUnits",
        "type": "uint256"
      }
    ],
    "name": "transferSharesByUnits",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "unpause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "oldReferrer",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "newReferrer",
        "type": "address"
      },
      {
        "internalType": "address[]",
        "name": "referredUsers",
        "type": "address[]"
      }
    ],
    "name": "updateReferrerForUsers",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/Context.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/access/IAccessControl.sol";
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/math/Math.sol"; // for mulDiv

/// @title HoodFrens
/// @author TopStrike Team
/// @notice This contract enables trading of player shares using a quadratic bonding curve with IPO mechanics
/// @dev Inherits from Ownable2Step, ReentrancyGuard, Pausable, and AccessControl for security and governance
contract HoodFrens is Ownable2Step, ReentrancyGuard, Pausable, AccessControl {
    ////////////////////////////////////////////////////////////////////
    // ROLES
    ////////////////////////////////////////////////////////////////////
    /// @notice Role for enabling/disabling trading for players
    bytes32 public constant ROLE_TRADING_TOGGLER =
        keccak256("ROLE_TRADING_TOGGLER");
    /// @notice Role for managing card prizes, adding players, and sniping shares to prize wallet
    bytes32 public constant ROLE_CARD_PRIZE_MANAGER =
        keccak256("ROLE_CARD_PRIZE_MANAGER");
    /// @notice Role for distributing ETH prizes to users
    bytes32 public constant ROLE_ETH_PRIZE_MANAGER =
        keccak256("ROLE_ETH_PRIZE_MANAGER");
    /// @notice Role for managing user referrers
    bytes32 public constant ROLE_REFERRER_MANAGER =
        keccak256("ROLE_REFERRER_MANAGER");
    /// @notice Role for cleaning up stale storage slots
    bytes32 public constant ROLE_STORAGE_CLEANER =
        keccak256("ROLE_STORAGE_CLEANER");

    ////////////////////////////////////////////////////////////////////
    // Wallets for prize and protocol fees
    ////////////////////////////////////////////////////////////////////
    /// @notice Address that receives prize pool fees
    address public prizeWallet;
    /// @notice Address that receives protocol fees
    address public protocolWallet;

    ////////////////////////////////////////////////////////////////////
    // Constants and fee configurations
    ////////////////////////////////////////////////////////////////////

    /// @notice Basis points denominator
    uint256 public constant BPS = 10000;

    /// @notice Decimal precision for shares (18 decimals, similar to ERC20)
    uint8 public constant SHARES_DECIMALS = 18;
    /// @notice One full share in smallest units (1e18)
    uint256 public constant ONE_FULL_SHARE_IN_UNITS = 10 ** SHARES_DECIMALS; // 1 full share

    /// @notice Denominator for the quadratic bonding curve formula
    uint256 public constant BONDING_CURVE_DENOMINATOR = 32000;

    // Precalculated helper constants for overflow safety
    uint256 private constant ONE_FULL_SHARE_IN_UNITS2 =
        ONE_FULL_SHARE_IN_UNITS * ONE_FULL_SHARE_IN_UNITS;
    uint256 private constant ONE_FULL_SHARE_IN_UNITS3 =
        ONE_FULL_SHARE_IN_UNITS2 * ONE_FULL_SHARE_IN_UNITS;

    /// @notice Total commission on sells in basis points (auto-calculated from prize + protocol fees)
    uint256 public COMMISSION_BPS = 890; // 8.9% total commission
    /// @notice Prize pool fee on sells in basis points (6% of gross sale)
    uint256 public SELL_PRIZE_FEE_BPS = 600; // 6% of gross sale (to prize pool)
    /// @notice Protocol fee on sells in basis points before referral deduction (2.9% of gross sale)
    uint256 public PROTOCOL_FEE_GROSS_BPS = 290; // 2.9% of gross sale (to protocol, reduced by 1% for referrer)
    /// @notice Referral fee in basis points (1% deducted from protocol fee if referrer exists)
    uint256 public constant REFERRAL_FEE_BPS = 100; // 1% referrer fee

    /// @notice Protocol fee charged during IPO buys in basis points (1.9%)
    uint256 public IPO_PROTOCOL_FEE_BPS = 190; // 1.9% to protocol
    /// @notice Prize fee charged during IPO buys in basis points (2.0%)
    uint256 public IPO_PRIZE_FEE_BPS = 200; // 2.0% to prize
    /// @notice Duration of IPO window where special fees apply
    uint256 public IPO_FEE_DURATION = 24 hours;

    /// @notice Maximum allowed prize fee on sells (10%)
    uint256 public constant MAX_SELL_PRIZE_FEE_BPS = 1000; // Max 10% for prize on sell
    /// @notice Maximum allowed protocol fee (5%)
    uint256 public constant MAX_PROTOCOL_FEE_BPS = 500; // Max 5% for protocol
    /// @notice Maximum allowed total commission on sells (15%)
    uint256 public constant MAX_TOTAL_COMMISSION_BPS = 1500; // Max 15% total commission
    /// @notice Maximum allowed IPO prize fee (10%)
    uint256 public constant MAX_IPO_PRIZE_FEE_BPS = 1000; // Max 10%
    /// @notice Maximum allowed IPO protocol fee (10%)
    uint256 public constant MAX_IPO_PROTOCOL_FEE_BPS = 1000; // Max 10%
    /// @notice Maximum allowed IPO fee duration (7 days)
    uint256 public constant MAX_IPO_FEE_DURATION = 7 days; // Max 7 days
    /// @notice Minimum allowed IPO fee duration (0 = disabled)
    uint256 public constant MIN_IPO_FEE_DURATION = 0; // Min 0 (disabled)

    /// @notice Size of one trading lot in units (0.1 shares)
    uint256 public LOT_SIZE_UNITS = ONE_FULL_SHARE_IN_UNITS / 10; // 0.1 shares, in 18-decimal units

    /// @notice Minimum lots per regular trade call
    uint256 public LOTS_PER_CALL_MIN = 1; // Min 1 lot per call
    /// @notice Maximum lots per regular trade call
    uint256 public LOTS_PER_CALL_MAX = 1; // Max 1 lot per call

    /// @notice Minimum lots per IPO trade call
    uint256 public IPO_LOTS_PER_CALL_MIN = 1; // Min 1 lot per IPO call
    /// @notice Maximum lots per IPO trade call
    uint256 public IPO_LOTS_PER_CALL_MAX = 1; // Max 1 lot per IPO call

    /// @notice Maximum total lots per wallet during IPO
    uint256 public IPO_MAX_LOTS_PER_WALLET = 5; // Max 5 lots per wallet during IPO
    /// @notice IPO rate limit in trades per second (linear accumulation)
    uint256 public IPO_TRADES_PER_SECOND = 3; // Linear rate: 3 trades/sec

    ////////////////////////////////////////////////////////////////////
    // Player structure
    ////////////////////////////////////////////////////////////////////
    /// @notice Player data structure
    /// @param name Player's name
    /// @param tradingEnabled Whether trading is enabled for this player
    /// @param ipoWindowStartTimestamp Timestamp when IPO window started
    /// @param ipoWindowEndTimestamp Timestamp when IPO window ended (0 if ongoing)
    struct Player {
        string name;
        bool tradingEnabled;
        uint256 ipoWindowStartTimestamp;
        uint256 ipoWindowEndTimestamp;
    }

    ////////////////////////////////////////////////////////////////////
    // Storage Cleanup - Struct
    ////////////////////////////////////////////////////////////////////
    /// @notice Struct for specifying cleanup targets
    /// @param playerId The player ID
    /// @param user The user address
    struct CleanupTarget {
        uint256 playerId;
        address user;
    }

    ////////////////////////////////////////////////////////////////////
    // Constants for change reasons
    ////////////////////////////////////////////////////////////////////
    
    /// @notice Reasons for balance and supply changes
    /// @dev Used to differentiate the cause of UserSharesChanged and PlayerSharesSupplyChanged events for off-chain indexing
    string constant REASON_BUY = "Buy";
    string constant REASON_SELL = "Sell";
    string constant REASON_PRIZE_AWARD = "PrizeAward";
    string constant REASON_TRANSFER = "Transfer";
    string constant REASON_PRIZE_WALLET_SNIPE = "PrizeWalletNewPlayerSnipe";

    ////////////////////////////////////////////////////////////////////
    // Player and shares management state
    ////////////////////////////////////////////////////////////////////
    /// @notice Mapping of player ID to Player data
    mapping(uint256 => Player) public players;
    /// @notice Counter for next player ID to assign
    uint256 public nextPlayerId;
    /// @notice Mapping of player ID and user address to share balance in units
    mapping(uint256 => mapping(address => uint256)) public sharesBalance; // In 18-decimal units
    /// @notice Mapping of player ID to total share supply in units
    mapping(uint256 => uint256) public sharesSupply; // In 18-decimal units
    /// @notice Mapping of user address to their referrer address
    mapping(address => address) public referrerOf;
    /// @notice Mapping of player ID and user address to total IPO lots purchased
    mapping(uint256 => mapping(address => uint256)) private _ipoLotsUsed; // Total IPO lots used per user
    /// @notice Mapping of player ID and user address to total IPO trades consumed (for rate limiting)
    mapping(uint256 => mapping(address => uint256)) private _ipoTradesUsed; // Total IPO trades consumed
    ////////////////////////////////////////////////////////////////////
    // Pause functionality
    ////////////////////////////////////////////////////////////////////
    /// @notice Reason string for why the contract is paused
    string public pauseReason;
    ////////////////////////////////////////////////////////////////////
    // New state variables
    ////////////////////////////////////////////////////////////////////
    /// @notice Total ETH prizes awarded to users
    uint256 public totalEthPrizesAwarded;

    ////////////////////////////////////////////////////////////////////
    // ETH Transfer Gas Limit Configuration
    ////////////////////////////////////////////////////////////////////
    /// @notice Default gas limit for ETH transfers
    uint256 public defaultTransferGasLimit;
    /// @notice Custom gas limits for specific recipients
    mapping(address => uint256) public customGasLimit;
    /// @notice Minimum allowed gas limit for transfers
    uint256 public constant MIN_TRANSFER_GAS = 2300;
    /// @notice Maximum allowed gas limit for transfers
    uint256 public constant MAX_TRANSFER_GAS = 100000;

    ////////////////////////////////////////////////////////////////////
    // Events
    ////////////////////////////////////////////////////////////////////
    /// @notice Emitted when a new player is added to the contract
    /// @param playerId The unique ID of the player
    /// @param name The name of the player
    /// @param snipeAmountInUnits The initial amount of shares sniped in units
    event PlayerAdded(
        uint256 indexed playerId,
        string name,
        uint256 snipeAmountInUnits
    );
    /// @notice Emitted when a player's IPO window status changes
    /// @param playerId The unique ID of the player
    /// @param name The name of the player
    /// @param startTimestamp The timestamp when IPO started
    /// @param endTimestamp The timestamp when IPO ended (0 if ongoing)
    /// @param currentSupply The current total supply for the player
    event PlayerIPO(
        uint256 indexed playerId,
        string name,
        uint256 startTimestamp,
        uint256 endTimestamp,
        uint256 currentSupply
    );
    /// @notice Emitted when a trade (buy or sell) occurs
    /// @dev priceInWei is wallet-facing value (NOT spot price):
    ///      - Buy: total ETH paid by trader (includes IPO fees if active)
    ///      - Sell: net ETH received by trader after sell fees
    ///      feeInWei is total fees charged for the trade:
    ///      - Buy: IPO fees only (prize + protocol), otherwise 0
    ///      - Sell: prize + protocol + optional referrer
    ///      For sells, gross pre-fee value can be derived as: gross = priceInWei + feeInWei.
    /// @param trader The address executing the trade
    /// @param playerId The player whose shares are being traded
    /// @param isBuy True if buying, false if selling
    /// @param amountInUnits The amount of shares traded in units
    /// @param priceInWei Wallet-facing ETH value:
    ///        - Buy: totalCost (base + IPO fees)
    ///        - Sell: netPayout (after fees)
    /// @param feeInWei Total fees charged in wei for this trade
    /// @param newSupplyInUnits The new total supply after the trade
    /// @param isIPOWindow True if trade occurred during IPO window
    event Trade(
        address indexed trader,
        uint256 indexed playerId,
        bool isBuy,
        uint256 amountInUnits,
        uint256 priceInWei,
        uint256 feeInWei,
        uint256 newSupplyInUnits,
        bool isIPOWindow
    );
    /// @notice Emitted when trading is enabled or disabled for a player
    /// @param playerId The player whose trading status changed
    /// @param enabled True if trading is enabled, false if disabled
    event TradingStatusUpdated(uint256 indexed playerId, bool enabled);
    /// @notice Emitted when shares are transferred between users
    /// @param from The address transferring shares
    /// @param to The address receiving shares
    /// @param playerId The player whose shares are being transferred
    /// @param amountInUnits The amount of shares transferred in units
    event SharesTransferred(
        address indexed from,
        address indexed to,
        uint256 indexed playerId,
        uint256 amountInUnits
    );
    /// @notice Emitted when ETH is deposited to the prize pool
    /// @param amountInWei The amount of ETH deposited in wei
    event EthPrizeDeposited(uint256 amountInWei);
    /// @notice Emitted when an ETH prize is awarded to a winner
    /// @param winner The address receiving the prize
    /// @param playerId The player associated with the prize
    /// @param amountInWei The amount of ETH awarded in wei
    /// @param description Description of the prize
    event EthPrizeAwarded(
        address indexed winner,
        uint256 indexed playerId,
        uint256 amountInWei,
        string description
    );
    /// @notice Emitted when share prizes are deposited for a player
    /// @param playerId The player whose shares are being deposited
    /// @param amountInUnits The amount of shares deposited in units
    event SharePrizeDeposited(uint256 indexed playerId, uint256 amountInUnits);
    /// @notice Emitted when share prizes are awarded to a winner
    /// @param winner The address receiving the shares
    /// @param playerId The player whose shares are being awarded
    /// @param amountInUnits The amount of shares awarded in units
    /// @param description Description of the prize
    event SharePrizeAwarded(
        address indexed winner,
        uint256 indexed playerId,
        uint256 amountInUnits,
        string description
    );
    /// @notice Emitted when a user's share balance changes for any reason
    /// @dev The 'reason' field enables off-chain tracking of share acquisition source:
    /// - "Buy": Shares purchased via buySharesByUnits/buySharesByLots
    /// - "Transfer": Shares received via transferSharesByUnits/transferSharesByLots
    /// - "PrizeAward": Shares received as prizes via awardSharePrize functions
    /// - "Sell": Shares sold (negative amountChangeInUnits)
    /// - "PrizeWalletNewPlayerSnipe": Initial prize pool snipe when player is added
    /// 
    /// This tracking allows competitions to differentiate between purchased shares
    /// vs transferred/gifted shares (which could be used to game eligibility requirements).
    /// 
    /// @param user The address whose balance changed
    /// @param playerId The player whose shares changed
    /// @param amountChangeInUnits The change in shares (positive for increase, negative for decrease)
    /// @param newUserBalanceInUnits The user's new total balance for this player
    /// @param totalCostInWei The total cost/proceeds in wei (for buys/sells)
    /// @param totalFeesInWei The total fees paid in wei (for buys/sells)
    /// @param netAmountInWei The net amount in wei after fees
    /// @param reason The reason for the balance change (for off-chain indexing)
    event UserSharesChanged(
        address indexed user,
        uint256 indexed playerId,
        int256 amountChangeInUnits,
        uint256 newUserBalanceInUnits,
        uint256 totalCostInWei,
        uint256 totalFeesInWei,
        uint256 netAmountInWei,
        string reason
    );
    /// @notice Emitted when a player's total share supply changes
    /// @param playerId The player whose supply changed
    /// @param supplyChangeInUnits The change in total supply (positive for increase, negative for decrease)
    /// @param newTotalSupplyInUnits The new total supply for this player
    /// @param currentPriceInWei The current price per share after the change
    /// @param marketCapInWei The total market cap after the change
    /// @param reason The reason for the supply change (for off-chain indexing)
    event PlayerSharesSupplyChanged(
        uint256 indexed playerId,
        int256 supplyChangeInUnits,
        uint256 newTotalSupplyInUnits,
        uint256 currentPriceInWei,
        uint256 marketCapInWei,
        string reason
    );
    /// @notice Emitted when a referrer is set for a user
    /// @param user The user whose referrer was set
    /// @param referrer The referrer address
    event ReferrerSet(address indexed user, address indexed referrer);
    /// @notice Emitted when all referrals are migrated from one address to another
    /// @param oldReferrer The old referrer address
    /// @param newReferrer The new referrer address
    /// @param referralCount The number of referrals migrated
    event ReferrerMigrated(address indexed oldReferrer, address indexed newReferrer, uint256 referralCount);
    /// @notice Emitted when a referral fee is paid
    /// @param referrer The address receiving the fee
    /// @param user The user whose trade generated the fee
    /// @param amountInWei The amount of the fee in wei
    event ReferralFeePaid(
        address indexed referrer,
        address indexed user,
        uint256 amountInWei
    );
    /// @notice Emitted when prize or protocol wallet addresses are updated
    /// @param newPrizeWallet The new prize wallet address
    /// @param newProtocolWallet The new protocol wallet address
    event WalletsUpdated(address newPrizeWallet, address newProtocolWallet);
    /// @notice Emitted when fee parameters are updated
    /// @param newSellPrizeFeeBps The new sell prize fee in basis points
    /// @param newProtocolFeeBps The new protocol fee in basis points
    /// @param newIpoPrizeFeeBps The new IPO prize fee in basis points
    /// @param newIpoProtocolFeeBps The new IPO protocol fee in basis points
    event FeesUpdated(
        uint256 newSellPrizeFeeBps,
        uint256 newProtocolFeeBps,
        uint256 newIpoPrizeFeeBps,
        uint256 newIpoProtocolFeeBps
    );
    /// @notice Emitted when trading parameters are updated
    /// @param newLotSizeUnits The new lot size in units
    /// @param newLotsPerCallMin The new minimum lots per call
    /// @param newLotsPerCallMax The new maximum lots per call
    event TradingParametersUpdated(
        uint256 newLotSizeUnits,
        uint256 newLotsPerCallMin,
        uint256 newLotsPerCallMax
    );
    /// @notice Emitted when IPO parameters are updated
    /// @param newIpoFeeDuration The new IPO fee duration
    /// @param newIpoLotsPerCallMin The new minimum IPO lots per call
    /// @param newIpoLotsPerCallMax The new maximum IPO lots per call
    /// @param newIpoMaxLotsPerWallet The new maximum IPO lots per wallet
    /// @param newIpoTradesPerSecond The new IPO trades per second rate limit
    event IpoParametersUpdated(
        uint256 newIpoFeeDuration,
        uint256 newIpoLotsPerCallMin,
        uint256 newIpoLotsPerCallMax,
        uint256 newIpoMaxLotsPerWallet,
        uint256 newIpoTradesPerSecond
    );
    /// @notice Emitted when the contract is paused
    /// @param reason The reason for pausing
    event ContractPaused(string reason);
    /// @notice Emitted when the contract is unpaused
    event ContractUnpaused();
    /// @notice Emitted when IPO buy fees are applied to a transaction
    /// @param user The user making the purchase
    /// @param playerId The player being purchased
    /// @param baseCostInWei The base cost before fees
    /// @param prizeFeeInWei The prize fee amount
    /// @param protocolFeeInWei The protocol fee amount
    /// @param totalCostInWei The total cost including fees
    event IpoBuyFeesApplied(
        address indexed user,
        uint256 indexed playerId,
        uint256 baseCostInWei,
        uint256 prizeFeeInWei,
        uint256 protocolFeeInWei,
        uint256 totalCostInWei
    );
    /// @notice Emitted when the default transfer gas limit is updated
    /// @param oldLimit The previous gas limit
    /// @param newLimit The new gas limit
    event DefaultTransferGasLimitUpdated(uint256 oldLimit, uint256 newLimit);
    /// @notice Emitted when a custom gas limit is set for a recipient
    /// @param recipient The address with the custom limit
    /// @param gasLimit The new gas limit
    event CustomGasLimitSet(address indexed recipient, uint256 gasLimit);
    /// @notice Emitted when a custom gas limit is removed for a recipient
    /// @param recipient The address whose custom limit was removed
    event CustomGasLimitRemoved(address indexed recipient);
    /// @notice Emitted when referral fee transfer fails and is redirected to protocol wallet
    /// @param intendedRecipient The referrer who should have received the fee
    /// @param seller The user whose sale generated the referral fee
    /// @param amount The amount redirected to protocol wallet
    event ReferralFeeRedirectedToProtocol(
        address indexed intendedRecipient,
        address indexed seller,
        uint256 amount
    );
    /// @notice Emitted when prize transfer fails and is refunded to sender
    /// @param intendedRecipient The winner who should have received the prize
    /// @param amount The amount refunded to the prize manager
    /// @param description The prize description
    event PrizeTransferFailed(
        address indexed intendedRecipient,
        uint256 amount,
        string description
    );

    ////////////////////////////////////////////////////////////////////
    // Storage Cleanup - Events
    ////////////////////////////////////////////////////////////////////
    /// @notice Emitted when a zero-balance storage slot is cleaned up
    /// @dev Off-chain indexers should deduplicate by [playerId, user] pair
    /// @param playerId The player whose balance slot was cleaned
    /// @param user The user whose zero balance was cleaned
    event ZeroBalanceCleaned(uint256 indexed playerId, address indexed user);

    /// @notice Emitted when IPO tracking data is cleaned up
    /// @param playerId The player whose IPO data was cleaned
    /// @param user The user whose IPO data was cleaned
    /// @param lotsUsed The lots value that was cleared (0 if already cleaned)
    /// @param tradesUsed The trades value that was cleared (0 if already cleaned)
    event IpoTrackingCleaned(
        uint256 indexed playerId,
        address indexed user,
        uint256 lotsUsed,
        uint256 tradesUsed
    );

    /// @notice Emitted after a batch cleanup operation completes
    /// @param cleanupType The type of cleanup performed ("ZeroBalances" or "IpoTracking")
    /// @param processed The number of targets processed
    event BatchCleanupCompleted(string cleanupType, uint256 processed);

    ////////////////////////////////////////////////////////////////////
    // Modifiers
    ////////////////////////////////////////////////////////////////////
    modifier playerExists(uint256 playerId) {
        require(playerId < nextPlayerId, "Player does not exist");
        _;
    }
    modifier onlyCardPrizeManager() {
        require(
            hasRole(ROLE_CARD_PRIZE_MANAGER, msg.sender),
            "Caller is not card prize manager"
        );
        _;
    }
    modifier onlyEthPrizeManager() {
        require(
            hasRole(ROLE_ETH_PRIZE_MANAGER, msg.sender),
            "Caller is not eth prize manager"
        );
        _;
    }
    modifier onlyReferrerManager() {
        require(
            hasRole(ROLE_REFERRER_MANAGER, msg.sender),
            "Caller is not referrer manager"
        );
        _;
    }
    modifier onlyTradingToggler() {
        require(
            hasRole(ROLE_TRADING_TOGGLER, msg.sender),
            "Caller is not trading toggler"
        );
        _;
    }
    modifier onlyStorageCleaner() {
        require(
            hasRole(ROLE_STORAGE_CLEANER, msg.sender),
            "Caller is not storage cleaner"
        );
        _;
    }

    ////////////////////////////////////////////////////////////////////
    // Constructor
    ////////////////////////////////////////////////////////////////////
    constructor(
        address _prizeWallet,
        address _protocolWallet
    ) Ownable(msg.sender) {
        require(
            _prizeWallet != address(0),
            "Prize wallet cannot be zero address"
        );
        require(
            _protocolWallet != address(0),
            "Protocol wallet cannot be zero address"
        );
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(ROLE_TRADING_TOGGLER, msg.sender);
        _grantRole(ROLE_CARD_PRIZE_MANAGER, msg.sender);
        _grantRole(ROLE_ETH_PRIZE_MANAGER, msg.sender);
        _grantRole(ROLE_REFERRER_MANAGER, msg.sender);
        _grantRole(ROLE_STORAGE_CLEANER, msg.sender);
        prizeWallet = _prizeWallet;
        protocolWallet = _protocolWallet;
        defaultTransferGasLimit = MIN_TRANSFER_GAS;
    }

    ////////////////////////////////////////////////////////////////////
    // Admin functions - Contract Control
    ////////////////////////////////////////////////////////////////////
    function pause(string calldata reason) external onlyOwner {
        require(bytes(reason).length > 0, "Reason required");
        _pause();
        pauseReason = reason;
        emit ContractPaused(reason);
    }

    function unpause() external onlyOwner {
        _unpause();
        pauseReason = "";
        emit ContractUnpaused();
    }

    ////////////////////////////////////////////////////////////////////
    // Admin functions - Wallet Configuration
    ////////////////////////////////////////////////////////////////////
    /// @notice Sets the prize wallet address
    /// @param _prize New prize wallet address
    function setPrizeWallet(address _prize) external onlyOwner {
        require(_prize != address(0), "Invalid address");
        prizeWallet = _prize;
        emit WalletsUpdated(_prize, protocolWallet);
    }

    /// @notice Sets the protocol wallet address
    /// @param _protocol New protocol wallet address
    function setProtocolWallet(address _protocol) external onlyOwner {
        require(_protocol != address(0), "Invalid address");
        protocolWallet = _protocol;
        emit WalletsUpdated(prizeWallet, _protocol);
    }

    ////////////////////////////////////////////////////////////////////
    // Admin functions - Transfer Gas Limit Configuration
    ////////////////////////////////////////////////////////////////////
    /// @notice Updates the global default gas limit for ETH transfers
    /// @param newLimit New default gas limit value
    function setDefaultTransferGasLimit(uint256 newLimit) external onlyRole(ROLE_CARD_PRIZE_MANAGER) {
        require(newLimit >= MIN_TRANSFER_GAS, "Below minimum gas limit");
        require(newLimit <= MAX_TRANSFER_GAS, "Exceeds maximum gas limit");

        uint256 oldLimit = defaultTransferGasLimit;
        defaultTransferGasLimit = newLimit;

        emit DefaultTransferGasLimitUpdated(oldLimit, newLimit);
    }

    /// @notice Sets a custom gas limit for a specific recipient
    /// @param recipient Address to set custom limit for
    /// @param gasLimit Custom gas limit (0 to use default)
    function setCustomGasLimit(address recipient, uint256 gasLimit) external onlyRole(ROLE_CARD_PRIZE_MANAGER) {
        require(recipient != address(0), "Invalid recipient");

        if (gasLimit == 0) {
            delete customGasLimit[recipient];
            emit CustomGasLimitRemoved(recipient);
        } else {
            require(gasLimit >= MIN_TRANSFER_GAS, "Below minimum gas limit");
            require(gasLimit <= MAX_TRANSFER_GAS, "Exceeds maximum gas limit");
            customGasLimit[recipient] = gasLimit;
            emit CustomGasLimitSet(recipient, gasLimit);
        }
    }

    /// @notice Sets custom gas limits for multiple recipients in one transaction
    /// @param recipients Array of addresses
    /// @param gasLimits Array of gas limits (0 to remove/use default)
    function setCustomGasLimitBatch(
        address[] calldata recipients,
        uint256[] calldata gasLimits
    ) external onlyRole(ROLE_CARD_PRIZE_MANAGER) {
        require(recipients.length == gasLimits.length, "Length mismatch");

        for (uint256 i = 0; i < recipients.length; i++) {
            address recipient = recipients[i];
            uint256 gasLimit = gasLimits[i];

            require(recipient != address(0), "Invalid recipient");

            if (gasLimit == 0) {
                delete customGasLimit[recipient];
                emit CustomGasLimitRemoved(recipient);
            } else {
                require(gasLimit >= MIN_TRANSFER_GAS, "Below minimum gas limit");
                require(gasLimit <= MAX_TRANSFER_GAS, "Exceeds maximum gas limit");
                customGasLimit[recipient] = gasLimit;
                emit CustomGasLimitSet(recipient, gasLimit);
            }
        }
    }

    /// @notice Returns the effective gas limit for a recipient
    /// @param recipient Address to check
    /// @return gasLimit The gas limit that will be used
    /// @return isCustom Whether this is a custom limit or the default
    function getEffectiveGasLimit(address recipient) external view returns (
        uint256 gasLimit,
        bool isCustom
    ) {
        uint256 custom = customGasLimit[recipient];
        if (custom > 0) {
            return (custom, true);
        }
        return (defaultTransferGasLimit, false);
    }

    ////////////////////////////////////////////////////////////////////
    // Admin functions - Fee Configuration
    ////////////////////////////////////////////////////////////////////
    /// @notice Sets fee parameters for the contract
    /// @param _sellPrizeFeeBps Prize fee on sells in basis points
    /// @param _protocolFeeGrossBps Protocol fee on sells in basis points (before referral deduction)
    /// @param _ipoPrizeFeeBps Prize fee on IPO buys in basis points
    /// @param _ipoProtocolFeeBps Protocol fee on IPO buys in basis points
    function setFees(
        uint256 _sellPrizeFeeBps,
        uint256 _protocolFeeGrossBps,
        uint256 _ipoPrizeFeeBps,
        uint256 _ipoProtocolFeeBps
    ) external onlyOwner {
        require(
            _sellPrizeFeeBps <= MAX_SELL_PRIZE_FEE_BPS,
            "Sell prize fee too high"
        );
        require(
            _protocolFeeGrossBps <= MAX_PROTOCOL_FEE_BPS,
            "Protocol fee too high"
        );
        require(
            _ipoPrizeFeeBps <= MAX_IPO_PRIZE_FEE_BPS,
            "IPO prize fee too high"
        );
        require(
            _ipoProtocolFeeBps <= MAX_IPO_PROTOCOL_FEE_BPS,
            "IPO protocol fee too high"
        );
        uint256 totalCommission = _sellPrizeFeeBps + _protocolFeeGrossBps;
        require(
            totalCommission <= MAX_TOTAL_COMMISSION_BPS,
            "Commission exceeds max"
        );
        SELL_PRIZE_FEE_BPS = _sellPrizeFeeBps;
        PROTOCOL_FEE_GROSS_BPS = _protocolFeeGrossBps;
        COMMISSION_BPS = totalCommission;
        IPO_PRIZE_FEE_BPS = _ipoPrizeFeeBps;
        IPO_PROTOCOL_FEE_BPS = _ipoProtocolFeeBps;
        emit FeesUpdated(
            SELL_PRIZE_FEE_BPS,
            PROTOCOL_FEE_GROSS_BPS,
            IPO_PRIZE_FEE_BPS,
            IPO_PROTOCOL_FEE_BPS
        );
    }

    ////////////////////////////////////////////////////////////////////
    // Admin functions - Trading Parameters
    ////////////////////////////////////////////////////////////////////
    /// @notice Sets trading parameters for the contract
    /// @param _lotSizeUnits Size of one lot in units
    /// @param _lotsPerCallMin Minimum lots per call
    /// @param _lotsPerCallMax Maximum lots per call
    function setTradingParameters(
        uint256 _lotSizeUnits,
        uint256 _lotsPerCallMin,
        uint256 _lotsPerCallMax
    ) external onlyOwner {
        require(
            _lotSizeUnits >= ONE_FULL_SHARE_IN_UNITS / 10000 &&
                _lotSizeUnits <= 10 * ONE_FULL_SHARE_IN_UNITS,
            "Lot size out of range"
        );
        require(
            ONE_FULL_SHARE_IN_UNITS % _lotSizeUnits == 0,
            "lot size must divide ONE_FULL_SHARE_IN_UNITS"
        );
        require(
            _lotsPerCallMin >= 1 && _lotsPerCallMin <= 100,
            "lots per call minimum out of range"
        );
        require(
            _lotsPerCallMax >= _lotsPerCallMin && _lotsPerCallMax <= 100,
            "lots per call maximum out of range"
        );

        LOT_SIZE_UNITS = _lotSizeUnits;
        LOTS_PER_CALL_MIN = _lotsPerCallMin;
        LOTS_PER_CALL_MAX = _lotsPerCallMax;
        emit TradingParametersUpdated(
            LOT_SIZE_UNITS,
            LOTS_PER_CALL_MIN,
            LOTS_PER_CALL_MAX
        );
    }

    ////////////////////////////////////////////////////////////////////
    // Admin functions - IPO Parameters
    ////////////////////////////////////////////////////////////////////
    /// @notice Sets IPO-specific parameters
    /// @param _ipoFeeDuration Duration of IPO fee window
    /// @param _ipoLotsPerCallMin Minimum IPO lots per call
    /// @param _ipoLotsPerCallMax Maximum IPO lots per call
    /// @param _ipoMaxLotsPerWallet Maximum total IPO lots per wallet
    /// @param _ipoTradesPerSecond IPO rate limit in trades per second
    function setIpoParameters(
        uint256 _ipoFeeDuration,
        uint256 _ipoLotsPerCallMin,
        uint256 _ipoLotsPerCallMax,
        uint256 _ipoMaxLotsPerWallet,
        uint256 _ipoTradesPerSecond
    ) external onlyOwner {
        require(
            _ipoFeeDuration >= MIN_IPO_FEE_DURATION && _ipoFeeDuration <= MAX_IPO_FEE_DURATION,
            "IPO fee duration out of range"
        );
        require(
            _ipoLotsPerCallMin >= 1 && _ipoLotsPerCallMin <= 100,
            "IPO lots per call minimum out of range"
        );
        require(
            _ipoLotsPerCallMax >= _ipoLotsPerCallMin && _ipoLotsPerCallMax <= 100,
            "IPO lots per call maximum out of range"
        );
        require(
            _ipoMaxLotsPerWallet >= 1 && _ipoMaxLotsPerWallet <= 1000,
            "IPO total lots per wallet maximum out of range"
        );
        require(
            _ipoTradesPerSecond >= 1 && _ipoTradesPerSecond <= 20,
            "IPO trades per second out of range"
        );

        IPO_FEE_DURATION = _ipoFeeDuration;
        IPO_LOTS_PER_CALL_MIN = _ipoLotsPerCallMin;
        IPO_LOTS_PER_CALL_MAX = _ipoLotsPerCallMax;
        IPO_MAX_LOTS_PER_WALLET = _ipoMaxLotsPerWallet;
        IPO_TRADES_PER_SECOND = _ipoTradesPerSecond;
        emit IpoParametersUpdated(
            IPO_FEE_DURATION,
            IPO_LOTS_PER_CALL_MIN,
            IPO_LOTS_PER_CALL_MAX,
            IPO_MAX_LOTS_PER_WALLET,
            IPO_TRADES_PER_SECOND
        );
    }

    ////////////////////////////////////////////////////////////////////
    // Referrer management functions
    ////////////////////////////////////////////////////////////////////
    
    /// @dev REFERRAL SYSTEM DESIGN PHILOSOPHY:
    /// 
    /// Referral attribution is wallet-based. This design intentionally allows the the user
    /// to transfer shares to new wallets if their original wallet is compromised,
    /// or if the platform migrates away from the current wallet provider
    /// The referrer manager can relink the referrer relationship using updateReferrerForUsers().
    ///
    /// COMPETITION ELIGIBILITY:
    /// Transferred shares are tracked separately from purchased shares via the "reason"
    /// field in UserSharesChanged events ("Transfer" vs "Buy"). Off-chain systems can
    /// differentiate between shares acquired via purchase vs transfer. This allows 
    /// competitions to require purchased shares only, preventing users from gaining 
    /// eligibility advantages by transferring shares between wallets.
    
    /// @notice Sets a referrer for a user
    /// @param user The user address
    /// @param referrer The referrer address
    function setUserReferrer(
        address user,
        address referrer
    ) external onlyReferrerManager {
        require(referrer != address(0), "Invalid referrer");
        require(
            referrerOf[user] == address(0),
            "Referrer already set for user"
        );
        require(user != address(0), "Invalid user address");
        require(
            referrer != user && referrer != address(this),
            "Invalid referrer - Cannot self refer"
        );
        referrerOf[user] = referrer;
        emit ReferrerSet(user, referrer);
    }

    /// @notice Sets referrers for multiple users in a single transaction
    /// @param users Array of user addresses
    /// @param referrers Array of referrer addresses (must match users length)
    function setMultipleUserReferrers(
        address[] calldata users,
        address[] calldata referrers
    ) external onlyReferrerManager {
        require(
            users.length == referrers.length,
            "Arrays must be equal length"
        );
        for (uint256 i = 0; i < users.length; i++) {
            address user = users[i];
            address referrer = referrers[i];
            require(
                user != address(0) && referrer != address(0),
                "Invalid address"
            );
            require(
                referrer != user && referrer != address(this),
                "Invalid referrer"
            );
            require(
                referrerOf[user] == address(0),
                "Referrer already set for user"
            );
            referrerOf[user] = referrer;
            emit ReferrerSet(user, referrer);
        }
    }

    /// @notice Migrate referrals from an old wallet to a new wallet when a user migrates wallets
    /// @dev This function handles relinking referrals should a user migrate to a new wallet address
    /// @param oldReferrer The old referrer address being migrated from
    /// @param newReferrer The new referrer address to migrate to
    /// @param referredUsers Array of users who were referred by the old referrer (can be single user or multiple users)
    function updateReferrerForUsers(
        address oldReferrer,
        address newReferrer,
        address[] calldata referredUsers
    ) external onlyReferrerManager {
        require(oldReferrer != address(0), "Invalid old referrer");
        require(newReferrer != address(0), "Invalid new referrer");
        require(oldReferrer != newReferrer, "Old and new referrer must be different");
        require(newReferrer != address(this), "Cannot refer to contract");
        require(referredUsers.length > 0, "Must provide at least one referred user");

        uint256 migratedCount = 0;
        for (uint256 i = 0; i < referredUsers.length; i++) {
            address user = referredUsers[i];
            require(user != address(0), "Invalid user address");
            require(referrerOf[user] == oldReferrer, "User not referred by old referrer");
            require(user != newReferrer, "Cannot self refer");
            
            referrerOf[user] = newReferrer;
            emit ReferrerSet(user, newReferrer);
            migratedCount++;
        }

        emit ReferrerMigrated(oldReferrer, newReferrer, migratedCount);
    }

    ////////////////////////////////////////////////////////////////////
    // Player and trading functions
    ////////////////////////////////////////////////////////////////////
    
    /// @notice Internal helper function to add a player and optionally snipe shares
    /// @param name The name of the player
    /// @param snipeAmountUnits The amount of shares to snipe in units (can be 0)
    function _addPlayerAndSnipeByUnits(
        string memory name,
        uint256 snipeAmountUnits
    ) internal nonReentrant {
        uint256 playerId = nextPlayerId;
        players[playerId] = Player(name, false, 0, 0);
        nextPlayerId++;
        emit PlayerAdded(playerId, name, snipeAmountUnits);
        if (snipeAmountUnits > 0) {
            require(snipeAmountUnits % LOT_SIZE_UNITS == 0, "bad step");
            uint256 cost = getBuyPriceRaw(playerId, snipeAmountUnits);
            require(msg.value >= cost, "Insufficient ETH for snipe");
            sharesBalance[playerId][msg.sender] += snipeAmountUnits;
            sharesSupply[playerId] += snipeAmountUnits;
            emit Trade(
                msg.sender,
                playerId,
                true,
                snipeAmountUnits,
                cost,
                0,
                sharesSupply[playerId],
                false
            );
            _emitUserSharesChanged(
                msg.sender,
                playerId,
                int256(snipeAmountUnits),
                cost,
                0,
                REASON_PRIZE_WALLET_SNIPE
            );
            _emitPlayerSupplyChanged(playerId, int256(snipeAmountUnits), REASON_PRIZE_WALLET_SNIPE);
            emit SharePrizeDeposited(playerId, snipeAmountUnits);
            if (msg.value > cost) {
                (bool success, ) = payable(msg.sender).call{
                    value: msg.value - cost
                }("");
                require(success, "Refund failed");
            }
        }

        // Refund if zero-snipe with ETH attached
        if (snipeAmountUnits == 0 && msg.value > 0) {
            (bool success, ) = payable(msg.sender).call{value: msg.value}("");
            require(success, "Refund failed");
        }
    }

    /// @notice Adds a new player and optionally snipes shares by units
    /// @param name The name of the player
    /// @param snipeAmountUnits The amount of shares to snipe in units (can be 0)
    function addPlayerAndSnipeByUnits(
        string memory name,
        uint256 snipeAmountUnits
    ) public payable onlyCardPrizeManager {
        _addPlayerAndSnipeByUnits(name, snipeAmountUnits);
    }

    /// @notice Adds a new player and optionally snipes shares by lots
    /// @param name The name of the player
    /// @param numLots The number of lots to snipe (can be 0)
    function addPlayerAndSnipeByLots(
        string memory name,
        uint256 numLots
    ) public payable onlyCardPrizeManager {
        require(numLots >= 0, "numLots must be positive");
        uint256 snipeAmountUnits = numLots * LOT_SIZE_UNITS;
        _addPlayerAndSnipeByUnits(name, snipeAmountUnits);
    }

    /// @notice Enables or disables trading for a player (starts IPO if first time enabled)
    /// @param playerId The player ID
    /// @param enabled True to enable trading, false to disable
    function setTradingStatus(
        uint256 playerId,
        bool enabled
    ) public playerExists(playerId) onlyTradingToggler {
        if (
            enabled &&
            !players[playerId].tradingEnabled &&
            players[playerId].ipoWindowStartTimestamp == 0
        ) {
            players[playerId].ipoWindowStartTimestamp = block.timestamp - 1;
            players[playerId].ipoWindowEndTimestamp =
                block.timestamp - 1 +
                IPO_FEE_DURATION;
            emit PlayerIPO(
                playerId,
                players[playerId].name,
                players[playerId].ipoWindowStartTimestamp,
                players[playerId].ipoWindowEndTimestamp,
                sharesSupply[playerId]
            );
        }
        players[playerId].tradingEnabled = enabled;
        emit TradingStatusUpdated(playerId, enabled);
    }

    /// @notice Enable or disable trading for multiple players at once
    /// @param playerIds Array of player IDs to update
    /// @param enabled true to enable trading (and start IPO if first time), false to disable
    /// @return updatedCount The number of players successfully updated
    function setTradingStatusBulk(
        uint256[] calldata playerIds,
        bool enabled
    ) external onlyTradingToggler returns (uint256 updatedCount) {
        require(playerIds.length > 0, "Must provide at least one player ID");
        
        for (uint256 i = 0; i < playerIds.length; i++) {
            uint256 playerId = playerIds[i];
            require(playerId < nextPlayerId, "Player does not exist");
            
            if (
                enabled &&
                !players[playerId].tradingEnabled &&
                players[playerId].ipoWindowStartTimestamp == 0
            ) {
                players[playerId].ipoWindowStartTimestamp = block.timestamp - 1;
                players[playerId].ipoWindowEndTimestamp =
                    block.timestamp - 1 +
                    IPO_FEE_DURATION;
                emit PlayerIPO(
                    playerId,
                    players[playerId].name,
                    players[playerId].ipoWindowStartTimestamp,
                    players[playerId].ipoWindowEndTimestamp,
                    sharesSupply[playerId]
                );
            }
            players[playerId].tradingEnabled = enabled;
            emit TradingStatusUpdated(playerId, enabled);
            updatedCount++;
        }
        
        return updatedCount;
    }

    /// @notice Buy fractional shares for a player in 18-decimal units
    /// @param playerId The ID of the player
    /// @param amountUnits The number of shares to buy in 18-decimal units
    function buySharesByUnits(
        uint256 playerId,
        uint256 amountUnits
    ) external payable whenNotPaused playerExists(playerId) {
        _buySharesUnits(playerId, amountUnits);
    }

    /// @notice Buy shares by specifying number of lots
    /// @param playerId The ID of the player
    /// @param numLots The number of lots to buy
    function buySharesByLots(
        uint256 playerId,
        uint256 numLots
    ) external payable whenNotPaused playerExists(playerId) {
        require(numLots > 0, "numLots must be positive");
        uint256 amountUnits = numLots * LOT_SIZE_UNITS;
        _buySharesUnits(playerId, amountUnits);
    }

    function _buySharesUnits(
        uint256 playerId,
        uint256 amountUnits
    ) internal nonReentrant {
        require(amountUnits > 0, "amount=0");
        require(amountUnits >= LOT_SIZE_UNITS, "below min size");
        require(amountUnits % LOT_SIZE_UNITS == 0, "bad step");
        uint256 lots = amountUnits / LOT_SIZE_UNITS;
        uint256 baseCost = getBuyPriceRaw(playerId, amountUnits);
        require(
            players[playerId].tradingEnabled || hasRole(ROLE_CARD_PRIZE_MANAGER, msg.sender),
            "Trading disabled"
        );

        if (
            isIpoPrizeFeeActive(playerId) &&
            !hasRole(ROLE_CARD_PRIZE_MANAGER, msg.sender)
        ) {
            // IPO-specific checks
            require(
                lots >= IPO_LOTS_PER_CALL_MIN && lots <= IPO_LOTS_PER_CALL_MAX,
                "IPO lots per call out of range"
            );
            require(
                _ipoLotsUsed[playerId][msg.sender] + lots <= IPO_MAX_LOTS_PER_WALLET,
                "Exceeds IPO lots per wallet limit"
            );
            // Linear rate limit: allowance accumulates from IPO start
            uint256 start = players[playerId].ipoWindowStartTimestamp;
            uint256 allowance = (block.timestamp - start) *
                IPO_TRADES_PER_SECOND; // floor by integer math
            require(
                _ipoTradesUsed[playerId][msg.sender] + 1 <= allowance,
                "IPO rate limit exceeded"
            );
            _ipoTradesUsed[playerId][msg.sender] += 1; // consume 1 from time-based budget
            _ipoLotsUsed[playerId][msg.sender] += lots; // consume lots from total-lots budget
        } else if (!hasRole(ROLE_CARD_PRIZE_MANAGER, msg.sender)) {
            // Non-IPO checks
            require(
                lots >= LOTS_PER_CALL_MIN && lots <= LOTS_PER_CALL_MAX,
                "Lots per call out of range"
            );
        }

        uint256 prizeFee = isIpoPrizeFeeActive(playerId)
            ? (baseCost * IPO_PRIZE_FEE_BPS) / BPS
            : 0;
        uint256 protFee = isIpoPrizeFeeActive(playerId)
            ? (baseCost * IPO_PROTOCOL_FEE_BPS) / BPS
            : 0;
        uint256 totalCost = baseCost + prizeFee + protFee;
        require(msg.value >= totalCost, "Insufficient ETH");

        sharesBalance[playerId][msg.sender] += amountUnits;
        sharesSupply[playerId] += amountUnits;
        emit Trade(
            msg.sender,
            playerId,
            true,
            amountUnits,
            totalCost,
            prizeFee + protFee,
            sharesSupply[playerId],
            isIpoPrizeFeeActive(playerId)
        );
        _emitUserSharesChanged(
            msg.sender,
            playerId,
            int256(amountUnits),
            totalCost,
            prizeFee + protFee,
            REASON_BUY
        );
        _emitPlayerSupplyChanged(playerId, int256(amountUnits), REASON_BUY);

        if (prizeFee > 0) {
            (bool ok1, ) = payable(prizeWallet).call{value: prizeFee}("");
            require(ok1, "Prize fee transfer failed");
            emit EthPrizeDeposited(prizeFee);
        }
        if (protFee > 0) {
            (bool ok2, ) = payable(protocolWallet).call{value: protFee}("");
            require(ok2, "Protocol IPO fee transfer failed");
        }
        if (prizeFee + protFee > 0) {
            emit IpoBuyFeesApplied(
                msg.sender,
                playerId,
                baseCost,
                prizeFee,
                protFee,
                totalCost
            );
        }
        if (msg.value > totalCost) {
            (bool success, ) = payable(msg.sender).call{
                value: msg.value - totalCost
            }("");
            require(success, "Refund failed");
        }
    }

    /// @notice Calculates the cost to buy `amountUnits` shares for a player, including IPO fees if applicable
    /// @param playerId The ID of the player
    /// @param amountUnits The number of shares to buy in 18-decimal units
    /// @return The total cost in wei, including base cost and any applicable IPO fees
    function getBuyPriceByUnits(
        uint256 playerId,
        uint256 amountUnits
    ) public view playerExists(playerId) returns (uint256) {
        uint256 baseCost = getBuyPriceRaw(playerId, amountUnits);
        if (isIpoPrizeFeeActive(playerId)) {
            uint256 prizeFee = (baseCost * IPO_PRIZE_FEE_BPS) / BPS;
            uint256 protFee = (baseCost * IPO_PROTOCOL_FEE_BPS) / BPS;
            return baseCost + prizeFee + protFee;
        }
        return baseCost;
    }

    /// @notice Calculates the raw cost to buy shares without fees (using bonding curve integral)
    /// @param playerId The ID of the player
    /// @param a The amount of shares in units
    /// @return The raw cost in wei (without fees)
    function getBuyPriceRaw(
        uint256 playerId,
        uint256 a
    ) public view playerExists(playerId) returns (uint256) {
        require(a > 0, "amount=0");
        uint256 s = sharesSupply[playerId]; // units (18d)
        uint256 num = _cube(s + a) - _cube(s);
        uint256 denom = 3 *
            BONDING_CURVE_DENOMINATOR *
            ONE_FULL_SHARE_IN_UNITS3;
        return Math.mulDiv(num, 1 ether, denom, Math.Rounding.Ceil);
    }

    /// @notice Calculates the gross payout for selling shares without fees (using bonding curve integral)
    /// @param playerId The ID of the player
    /// @param a The amount of shares in units
    /// @return The gross payout in wei (before fees)
    function calculateGrossSellPayoutByUnits(
        uint256 playerId,
        uint256 a
    ) public view playerExists(playerId) returns (uint256) {
        require(a > 0, "amount=0");
        uint256 s = sharesSupply[playerId];
        require(s >= a, "Not enough supply");
        uint256 num = _cube(s) - _cube(s - a);
        uint256 denom = 3 *
            BONDING_CURVE_DENOMINATOR *
            ONE_FULL_SHARE_IN_UNITS3;
        return Math.mulDiv(num, 1 ether, denom);
    }

    /// @notice Get the price to buy a specific number of lots
    /// @param playerId The ID of the player
    /// @param numLots The number of lots
    /// @return The total cost in wei, including any applicable IPO fees
    function getBuyPriceByLots(
        uint256 playerId,
        uint256 numLots
    ) external view playerExists(playerId) returns (uint256) {
        require(numLots > 0, "numLots must be positive");
        uint256 amountUnits = numLots * LOT_SIZE_UNITS;
        return getBuyPriceByUnits(playerId, amountUnits);
    }

    /// @notice Get the gross payout for selling a specific number of lots
    /// @param playerId The ID of the player
    /// @param numLots The number of lots
    /// @return The gross payout in wei (before fees)
    function calculateGrossSellPayoutByLots(
        uint256 playerId,
        uint256 numLots
    ) external view playerExists(playerId) returns (uint256) {
        require(numLots > 0, "numLots must be positive");
        uint256 amountUnits = numLots * LOT_SIZE_UNITS;
        return calculateGrossSellPayoutByUnits(playerId, amountUnits);
    }


    function _square(uint256 x) internal pure returns (uint256) {
        return Math.mulDiv(x, x, 1); // 512-bit intermediate, no overflow
    }

    function _cube(uint256 x) internal pure returns (uint256) {
        return Math.mulDiv(_square(x), x, 1); // 512-bit intermediate, no overflow
    }

    /// @notice Sell fractional shares for a player in 18-decimal units
    /// @param playerId The ID of the player
    /// @param amountUnits The number of shares to sell in 18-decimal units
    /// @param minNetPayout Minimum net payout (after fees) the seller is willing to accept
    function sellSharesByUnits(
        uint256 playerId,
        uint256 amountUnits,
        uint256 minNetPayout
    ) external whenNotPaused playerExists(playerId) {
        _sellSharesUnits(playerId, amountUnits, minNetPayout);
    }

    /// @notice Sell shares by specifying number of lots
    /// @param playerId The ID of the player
    /// @param numLots The number of lots to sell
    /// @param minNetPayout Minimum net payout (after fees) the seller is willing to accept
    function sellSharesByLots(
        uint256 playerId,
        uint256 numLots,
        uint256 minNetPayout
    ) external whenNotPaused playerExists(playerId) {
        require(numLots > 0, "numLots must be positive");
        uint256 amountUnits = numLots * LOT_SIZE_UNITS;
        _sellSharesUnits(playerId, amountUnits, minNetPayout);
    }

    /// @notice Core sell logic without lot size validation
    /// @dev Internal function that executes the actual sell mechanics
    /// @param playerId The ID of the player
    /// @param amountUnits The amount to sell in units
    /// @param minNetPayout Minimum net payout after fees
    function _executeSell(
        uint256 playerId,
        uint256 amountUnits,
        uint256 minNetPayout
    ) internal {
        require(amountUnits > 0, "amount=0");
        require(
            players[playerId].tradingEnabled || hasRole(ROLE_CARD_PRIZE_MANAGER, msg.sender),
            "Trading disabled"
        );
        require(
            sharesBalance[playerId][msg.sender] >= amountUnits,
            "Not enough shares"
        );

        uint256 grossPayout = calculateGrossSellPayoutByUnits(playerId, amountUnits);

        sharesBalance[playerId][msg.sender] -= amountUnits;
        sharesSupply[playerId] -= amountUnits;

        uint256 toPrize = (grossPayout * SELL_PRIZE_FEE_BPS) / BPS;
        uint256 toProtocol = (grossPayout * PROTOCOL_FEE_GROSS_BPS) / BPS;
        uint256 toReferrer = 0;

        address referrer = (referrerOf[msg.sender] != address(0) &&
            referrerOf[msg.sender] != msg.sender)
            ? referrerOf[msg.sender]
            : protocolWallet;

        if (referrer != protocolWallet) {
            toReferrer = (grossPayout * REFERRAL_FEE_BPS) / BPS;
            toProtocol -= toReferrer;
        }

        uint256 netPayout = grossPayout - toPrize - toProtocol - toReferrer;
        require(netPayout >= minNetPayout, "Slippage: payout below minimum");
        require(
            address(this).balance >= toPrize + toProtocol + toReferrer + netPayout,
            "Insufficient balance"
        );

        uint256 supplyAfter = sharesSupply[playerId];
        uint256 totalFees = toPrize + toProtocol + toReferrer;
        bool ipoActive = isIpoPrizeFeeActive(playerId);

        emit Trade(
            msg.sender,
            playerId,
            false,
            amountUnits,
            netPayout,
            totalFees,
            supplyAfter,
            ipoActive
        );

        _emitUserSharesChanged(
            msg.sender,
            playerId,
            -int256(amountUnits),
            grossPayout,
            totalFees,
            REASON_SELL
        );
        _emitPlayerSupplyChanged(playerId, -int256(amountUnits), REASON_SELL);

        emit EthPrizeDeposited(toPrize);

        (bool s1, ) = payable(prizeWallet).call{value: toPrize}("");
        require(s1, "Prize collection failed");
        (bool s2, ) = payable(protocolWallet).call{value: toProtocol}("");
        require(s2, "Protocol payout failed");
        if (toReferrer > 0) {
            bool referralSuccess = _transferReferralFee(referrer, toReferrer, msg.sender);
            if (referralSuccess) {
                emit ReferralFeePaid(referrer, msg.sender, toReferrer);
            }
        }
        (bool s4, ) = payable(msg.sender).call{value: netPayout}("");
        require(s4, "User payout failed");
    }

    /// @notice Sells shares with lot size validation
    function _sellSharesUnits(
        uint256 playerId,
        uint256 amountUnits,
        uint256 minNetPayout
    ) internal nonReentrant {
        require(amountUnits >= LOT_SIZE_UNITS, "below min size");
        require(amountUnits % LOT_SIZE_UNITS == 0, "bad step");

        uint256 lots = amountUnits / LOT_SIZE_UNITS;
        if (!hasRole(ROLE_CARD_PRIZE_MANAGER, msg.sender)) {
            require(
                lots >= LOTS_PER_CALL_MIN && lots <= LOTS_PER_CALL_MAX,
                "Lots out of range"
            );
        }

        _executeSell(playerId, amountUnits, minNetPayout);
    }

    /// @notice Sells only the fractional remainder that doesn't align with current LOT_SIZE_UNITS
    /// @dev Allows users to clean up balances that became misaligned after LOT_SIZE_UNITS changes
    /// @param playerId The ID of the player
    /// @param minNetPayout Minimum net payout (after fees) the seller is willing to accept
    function sellFractionalRemainder(
        uint256 playerId,
        uint256 minNetPayout
    ) external whenNotPaused playerExists(playerId) nonReentrant {
        uint256 balance = sharesBalance[playerId][msg.sender];
        uint256 remainder = balance % LOT_SIZE_UNITS;
        require(remainder > 0, "No fractional remainder");

        _executeSell(playerId, remainder, minNetPayout);
    }

    /// @notice Transfer fractional shares to another address in 18-decimal units
    /// @dev This supports wallet migration.
    /// 
    /// COMPETITION ELIGIBILITY: Transferred shares are flagged with reason="Transfer" in
    /// UserSharesChanged events, allowing off-chain systems to distinguish them from 
    /// purchased shares (reason="Buy"). Competitions may require purchased shares only,
    /// preventing users from gaining eligibility by transferring shares between wallets.
    /// 
    /// @param to The recipient address
    /// @param playerId The ID of the player
    /// @param amountUnits The number of shares to transfer in 18-decimal units
    function transferSharesByUnits(
        address to,
        uint256 playerId,
        uint256 amountUnits
    ) public whenNotPaused playerExists(playerId) {
        require(to != address(0), "Cannot transfer to zero address");
        require(to != msg.sender, "Cannot transfer to self");
        require(to != address(this), "Cannot transfer to contract");
        require(amountUnits > 0, "amount=0");
        require(amountUnits >= LOT_SIZE_UNITS, "below min size");
        require(amountUnits % LOT_SIZE_UNITS == 0, "bad step");
        require(
            sharesBalance[playerId][msg.sender] >= amountUnits,
            "Not enough shares"
        );
        sharesBalance[playerId][msg.sender] -= amountUnits;
        sharesBalance[playerId][to] += amountUnits;
        emit SharesTransferred(msg.sender, to, playerId, amountUnits);
        _emitTransferSharesChanged(msg.sender, to, playerId, amountUnits, REASON_TRANSFER);
    }

    /// @notice Transfer shares by specifying number of lots
    /// @param to The recipient address
    /// @param playerId The ID of the player
    /// @param numLots The number of lots to transfer
    function transferSharesByLots(
        address to,
        uint256 playerId,
        uint256 numLots
    ) external whenNotPaused playerExists(playerId) {
        require(numLots > 0, "numLots must be positive");
        uint256 amountUnits = numLots * LOT_SIZE_UNITS;
        transferSharesByUnits(to, playerId, amountUnits);
    }

    /// @notice Awards share prizes to a winner in 18-decimal units (internal function)
    /// @param winner The recipient address
    /// @param playerId The ID of the player
    /// @param amountUnits The number of shares to award in 18-decimal units
    /// @param description The prize description
    function awardSharePrizeByUnits(
        address winner,
        uint256 playerId,
        uint256 amountUnits,
        string calldata description
    ) internal whenNotPaused playerExists(playerId) onlyCardPrizeManager {
        require(amountUnits > 0, "amount=0");
        require(amountUnits >= LOT_SIZE_UNITS, "below min size");
        require(amountUnits % LOT_SIZE_UNITS == 0, "bad step");
        require(
            sharesBalance[playerId][msg.sender] >= amountUnits,
            "Not enough shares"
        );
        sharesBalance[playerId][msg.sender] -= amountUnits;
        sharesBalance[playerId][winner] += amountUnits;
        emit SharesTransferred(msg.sender, winner, playerId, amountUnits);
        emit SharePrizeAwarded(winner, playerId, amountUnits, description);
        _emitTransferSharesChanged(msg.sender, winner, playerId, amountUnits, REASON_PRIZE_AWARD);
    }

    /// @notice Award share prizes by specifying number of lots
    /// @param winner The recipient address
    /// @param playerId The ID of the player
    /// @param numLots The number of lots to award
    /// @param description The prize description
    function awardSharePrizeByLots(
        address winner,
        uint256 playerId,
        uint256 numLots,
        string calldata description
    ) external {
        require(numLots > 0, "numLots must be positive");
        uint256 amountUnits = numLots * LOT_SIZE_UNITS;
        awardSharePrizeByUnits(winner, playerId, amountUnits, description);
    }

    /// @notice Award share prizes from multiple players to multiple winners
    /// @param playerIds Array of player IDs (must match other arrays length)
    /// @param winners Array of winner addresses (must match other arrays length)
    /// @param amountUnits Array of share amounts in 18-decimal units (must match other arrays length)
    /// @param descriptions Array of prize descriptions (must match other arrays length)
    function awardSharePrizeBatchMultiPlayerMultiUser(
        uint256[] calldata playerIds,
        address[] calldata winners,
        uint256[] calldata amountUnits,
        string[] calldata descriptions
    ) external whenNotPaused onlyCardPrizeManager nonReentrant {
        require(playerIds.length > 0, "Must provide at least one prize");
        require(
            playerIds.length == winners.length &&
            playerIds.length == amountUnits.length &&
            playerIds.length == descriptions.length,
            "Arrays must be equal length"
        );

        for (uint256 i = 0; i < playerIds.length; i++) {
            uint256 playerId = playerIds[i];
            address winner = winners[i];
            uint256 amount = amountUnits[i];
            string calldata description = descriptions[i];

            require(playerId < nextPlayerId, "Player does not exist");
            require(winner != address(0), "Invalid winner address");
            require(amount > 0, "amount=0");
            require(amount >= LOT_SIZE_UNITS, "below min size");
            require(amount % LOT_SIZE_UNITS == 0, "bad step");
            
            require(
                sharesBalance[playerId][msg.sender] >= amount,
                "Not enough shares"
            );

            sharesBalance[playerId][msg.sender] -= amount;
            sharesBalance[playerId][winner] += amount;
            
            emit SharesTransferred(msg.sender, winner, playerId, amount);
            emit SharePrizeAwarded(winner, playerId, amount, description);
            _emitTransferSharesChanged(msg.sender, winner, playerId, amount, REASON_PRIZE_AWARD);
        }
    }

    /// @notice Award share prizes from multiple players to multiple winners (by lots)
    /// @param playerIds Array of player IDs (must match other arrays length)
    /// @param winners Array of winner addresses (must match other arrays length)
    /// @param numLots Array of number of lots (must match other arrays length)
    /// @param descriptions Array of prize descriptions (must match other arrays length)
    function awardSharePrizeBatchMultiPlayerMultiUserByLots(
        uint256[] calldata playerIds,
        address[] calldata winners,
        uint256[] calldata numLots,
        string[] calldata descriptions
    ) external whenNotPaused onlyCardPrizeManager nonReentrant {
        require(playerIds.length > 0, "Must provide at least one prize");
        require(
            playerIds.length == winners.length &&
            playerIds.length == numLots.length &&
            playerIds.length == descriptions.length,
            "Arrays must be equal length"
        );

        for (uint256 i = 0; i < playerIds.length; i++) {
            uint256 playerId = playerIds[i];
            address winner = winners[i];
            uint256 lots = numLots[i];
            string calldata description = descriptions[i];

            require(playerId < nextPlayerId, "Player does not exist");
            require(winner != address(0), "Invalid winner address");
            require(lots > 0, "numLots must be positive");
            
            uint256 amount = lots * LOT_SIZE_UNITS;
            
            require(
                sharesBalance[playerId][msg.sender] >= amount,
                "Not enough shares"
            );

            sharesBalance[playerId][msg.sender] -= amount;
            sharesBalance[playerId][winner] += amount;
            
            emit SharesTransferred(msg.sender, winner, playerId, amount);
            emit SharePrizeAwarded(winner, playerId, amount, description);
            _emitTransferSharesChanged(msg.sender, winner, playerId, amount, REASON_PRIZE_AWARD);
        }
    }

    ////////////////////////////////////////////////////////////////////
    // Prize management functions
    ////////////////////////////////////////////////////////////////////
    /// @notice Awards ETH prize to a winner (with player association)
    /// @param winner The address receiving the prize
    /// @param amount The amount of ETH to award in wei
    /// @param description Description of the prize
    /// @param playerId The player ID to associate the prize with
    function awardEthPrize(
        address winner,
        uint256 amount,
        string calldata description,
        uint256 playerId
    ) external payable whenNotPaused onlyEthPrizeManager nonReentrant {
        require(winner != address(0), "Invalid winner address");
        require(msg.value == amount, "ETH sent must match prize amount");
        bool success = _transferPrize(winner, amount, description);
        if (success) {
            totalEthPrizesAwarded += amount;
            emit EthPrizeAwarded(winner, playerId, amount, description);
        }
        // If failed, ETH is refunded to sender and PrizeTransferFailed was emitted
    }

    /// @notice Awards ETH prize to a winner (without player association)
    /// @param winner The address receiving the prize
    /// @param amount The amount of ETH to award in wei
    /// @param description Description of the prize
    function awardEthPrize(
        address winner,
        uint256 amount,
        string calldata description
    ) external payable whenNotPaused onlyEthPrizeManager nonReentrant {
        require(winner != address(0), "Invalid winner address");
        require(msg.value == amount, "ETH sent must match prize amount");
        bool success = _transferPrize(winner, amount, description);
        if (success) {
            totalEthPrizesAwarded += amount;
            emit EthPrizeAwarded(winner, 0, amount, description);
        }
        // If failed, ETH is refunded to sender and PrizeTransferFailed was emitted
    }

    /// @notice Records an ETH prize that was awarded externally (with player association)
    /// @param winner The address that received the prize
    /// @param amount The amount of ETH awarded in wei
    /// @param description Description of the prize
    /// @param playerId The player ID to associate the prize with
    function recordEthPrizeAwarded(
        address winner,
        uint256 amount,
        string calldata description,
        uint256 playerId
    ) external onlyEthPrizeManager {
        emit EthPrizeAwarded(winner, playerId, amount, description);
        totalEthPrizesAwarded += amount;
    }

    /// @notice Records an ETH prize that was awarded externally (without player association)
    /// @param winner The address that received the prize
    /// @param amount The amount of ETH awarded in wei
    /// @param description Description of the prize
    function recordEthPrizeAwarded(
        address winner,
        uint256 amount,
        string calldata description
    ) external onlyEthPrizeManager {
        emit EthPrizeAwarded(winner, 0, amount, description);
        totalEthPrizesAwarded += amount;
    }

    /// @notice Award ETH prizes to multiple winners in a single transaction
    /// @param winners Array of winner addresses
    /// @param amounts Array of prize amounts in wei (must match winners length)
    /// @param descriptions Array of prize descriptions (must match winners length)
    /// @param playerId The player ID to associate prizes with (0 for global/non-player prizes)
    function awardEthPrizeBatch(
        address[] calldata winners,
        uint256[] calldata amounts,
        string[] calldata descriptions,
        uint256 playerId
    ) external payable whenNotPaused onlyEthPrizeManager nonReentrant {
        require(winners.length > 0, "Must provide at least one winner");
        require(
            winners.length == amounts.length && winners.length == descriptions.length,
            "Arrays must be equal length"
        );

        uint256 totalAmount = 0;
        for (uint256 i = 0; i < amounts.length; i++) {
            totalAmount += amounts[i];
        }
        require(msg.value == totalAmount, "ETH sent must match total prize amount");

        for (uint256 i = 0; i < winners.length; i++) {
            address winner = winners[i];
            uint256 amount = amounts[i];
            string calldata description = descriptions[i];

            require(winner != address(0), "Invalid winner address");
            require(amount > 0, "Prize amount must be positive");

            bool success = _transferPrize(winner, amount, description);
            if (success) {
                totalEthPrizesAwarded += amount;
                emit EthPrizeAwarded(winner, playerId, amount, description);
            }
            // If failed, ETH is refunded to sender and PrizeTransferFailed was emitted
        }
    }

    /// @notice Awards ETH prizes to multiple winners in a single transaction (without player association)
    /// @param winners Array of winner addresses
    /// @param amounts Array of prize amounts in wei (must match winners length)
    /// @param descriptions Array of prize descriptions (must match winners length)
    function awardEthPrizeBatch(
        address[] calldata winners,
        uint256[] calldata amounts,
        string[] calldata descriptions
    ) external payable whenNotPaused onlyEthPrizeManager nonReentrant {
        require(winners.length > 0, "Must provide at least one winner");
        require(
            winners.length == amounts.length && winners.length == descriptions.length,
            "Arrays must be equal length"
        );

        uint256 totalAmount = 0;
        for (uint256 i = 0; i < amounts.length; i++) {
            totalAmount += amounts[i];
        }
        require(msg.value == totalAmount, "ETH sent must match total prize amount");

        for (uint256 i = 0; i < winners.length; i++) {
            address winner = winners[i];
            uint256 amount = amounts[i];
            string calldata description = descriptions[i];

            require(winner != address(0), "Invalid winner address");
            require(amount > 0, "Prize amount must be positive");

            bool success = _transferPrize(winner, amount, description);
            if (success) {
                totalEthPrizesAwarded += amount;
                emit EthPrizeAwarded(winner, 0, amount, description);
            }
            // If failed, ETH is refunded to sender and PrizeTransferFailed was emitted
        }
    }

    ////////////////////////////////////////////////////////////////////
    // Transfer functions
    ////////////////////////////////////////////////////////////////////
    
    /// @notice Transfer all shares of a specific player from caller to a specified address
    /// @param to The recipient address
    /// @param playerId The ID of the player whose shares to transfer
    function transferAllSharesOfIndividualPlayer(address to, uint256 playerId) external whenNotPaused nonReentrant playerExists(playerId) {
        require(to != address(0), "Cannot transfer to zero address");
        require(to != msg.sender, "Cannot transfer to self");
        require(to != address(this), "Cannot transfer to contract");
        
        uint256 balance = sharesBalance[playerId][msg.sender];
        require(balance > 0, "No shares to transfer");
        
        sharesBalance[playerId][msg.sender] = 0;
        sharesBalance[playerId][to] += balance;
        
        emit SharesTransferred(msg.sender, to, playerId, balance);
        _emitTransferSharesChanged(msg.sender, to, playerId, balance, REASON_TRANSFER);
    }
    
    /// @notice Transfer all shares from caller to a specified address
    /// @param to The recipient address
    function transferAllSharesInWallet(address to) external whenNotPaused nonReentrant {
        require(to != address(0), "Cannot transfer to zero address");
        require(to != msg.sender, "Cannot transfer to self");
        require(to != address(this), "Cannot transfer to contract");
        
        uint256 transferCount = 0;
        for (uint256 playerId = 0; playerId < nextPlayerId; playerId++) {
            uint256 balance = sharesBalance[playerId][msg.sender];
            if (balance > 0) {
                sharesBalance[playerId][msg.sender] = 0;
                sharesBalance[playerId][to] += balance;
                emit SharesTransferred(msg.sender, to, playerId, balance);
                _emitTransferSharesChanged(msg.sender, to, playerId, balance, REASON_TRANSFER);
                transferCount++;
            }
        }
        
        require(transferCount > 0, "No shares to transfer");
    }

    ////////////////////////////////////////////////////////////////////
    // Internal ETH Transfer Helpers
    ////////////////////////////////////////////////////////////////////
    /// @notice Gets the effective gas limit for a recipient
    /// @param recipient Address to get gas limit for
    /// @return Gas limit to use (custom if set, otherwise default)
    function _getGasLimitFor(address recipient) internal view returns (uint256) {
        uint256 custom = customGasLimit[recipient];
        return custom > 0 ? custom : defaultTransferGasLimit;
    }

    /// @notice Transfers referral fee with gas limit, redirects to protocol wallet if transfer fails
    /// @param referrer Referrer address
    /// @param amount Amount in wei
    /// @param seller The user whose sale generated the referral fee
    /// @return success Whether the transfer to the referrer succeeded
    function _transferReferralFee(
        address referrer,
        uint256 amount,
        address seller
    ) internal returns (bool success) {
        uint256 gasLimit = _getGasLimitFor(referrer);
        (success, ) = payable(referrer).call{value: amount, gas: gasLimit}("");

        if (!success) {
            // Redirect to protocol wallet
            (bool protocolSuccess, ) = payable(protocolWallet).call{value: amount}("");
            require(protocolSuccess, "Protocol fallback failed");
            emit ReferralFeeRedirectedToProtocol(referrer, seller, amount);
        }
    }

    /// @notice Transfers prize with gas limit, refunds sender if transfer fails
    /// @param winner Winner address
    /// @param amount Amount in wei
    /// @param description Prize description for event
    /// @return success Whether the transfer succeeded
    function _transferPrize(
        address winner,
        uint256 amount,
        string calldata description
    ) internal returns (bool success) {
        uint256 gasLimit = _getGasLimitFor(winner);
        (success, ) = payable(winner).call{value: amount, gas: gasLimit}("");

        if (!success) {
            // Refund to sender (prize manager)
            (bool refundSuccess, ) = payable(msg.sender).call{value: amount}("");
            require(refundSuccess, "Prize refund failed");
            emit PrizeTransferFailed(winner, amount, description);
        }
    }

    ////////////////////////////////////////////////////////////////////
    // Utility functions
    ////////////////////////////////////////////////////////////////////
    /// @notice Gets a user's share balance for a player in units
    /// @param user The user address
    /// @param playerId The player ID
    /// @return The share balance in 18-decimal units
    function getUserPlayerHoldingInUnits(
        address user,
        uint256 playerId
    ) public view playerExists(playerId) returns (uint256) {
        return sharesBalance[playerId][user];
    }

    /// @notice Gets a user's share balance for a player in full shares
    /// @param user The user address
    /// @param playerId The player ID
    /// @return The share balance in full shares (units / 1e18)
    function getUserPlayerHoldingInFullShares(
        address user,
        uint256 playerId
    ) public view playerExists(playerId) returns (uint256) {
        return sharesBalance[playerId][user] / ONE_FULL_SHARE_IN_UNITS; // Convert to user-friendly units (18 decimals to whole shares)
    }

    /// @notice Gets a user's share balance for a player in lots
    /// @param user The user address
    /// @param playerId The player ID
    /// @return The share balance in lots
    function getUserPlayerHoldingInLots(
        address user,
        uint256 playerId
    ) public view playerExists(playerId) returns (uint256) {
        return sharesBalance[playerId][user] / LOT_SIZE_UNITS; // Return in lots
    }

    /// @notice Gets players with pagination
    /// @param offset The starting index
    /// @param limit The maximum number of players to return
    /// @return ids Array of player IDs
    /// @return names Array of player names
    /// @return tradingEnableds Array of trading enabled flags
    function getAllPlayers(
        uint256 offset,
        uint256 limit
    )
        public
        view
        returns (
            uint256[] memory ids,
            string[] memory names,
            bool[] memory tradingEnableds
        )
    {
        uint256 total = nextPlayerId;
        if (offset >= total || limit == 0) {
            return (new uint256[](0), new string[](0), new bool[](0));
        }
        uint256 end = offset + limit;
        if (end > total) {
            end = total;
        }
        uint256 count = end - offset;
        ids = new uint256[](count);
        names = new string[](count);
        tradingEnableds = new bool[](count);
        for (uint256 i = 0; i < count; i++) {
            uint256 playerId = offset + i;
            Player storage p = players[playerId];
            ids[i] = playerId;
            names[i] = p.name;
            tradingEnableds[i] = p.tradingEnabled;
        }
        return (ids, names, tradingEnableds);
    }

    /// @notice Checks if IPO prize fee is currently active for a player
    /// @param playerId The player ID
    /// @return True if within IPO fee window, false otherwise
    function isIpoPrizeFeeActive(
        uint256 playerId
    ) public view playerExists(playerId) returns (bool) {
        return
            block.timestamp >= players[playerId].ipoWindowStartTimestamp &&
            block.timestamp <= players[playerId].ipoWindowEndTimestamp;
    }

    /// @notice Gets the end time of IPO prize fee window for a player
    /// @param playerId The player ID
    /// @return The timestamp when IPO fee window ends
    function getIpoPrizeFeeEndTime(
        uint256 playerId
    ) public view playerExists(playerId) returns (uint256) {
        return players[playerId].ipoWindowEndTimestamp;
    }

    /// @notice Gets a user's portfolio holdings in units with pagination
    /// @param user The user address
    /// @param offset The starting index (in terms of holdings found, not player IDs)
    /// @param limit The maximum number of holdings to return
    /// @return playerIds Array of player IDs where user has holdings
    /// @return quantities Array of quantities in units (18 decimals)
    function getPortfolioHoldingsInUnits(
        address user,
        uint256 offset,
        uint256 limit
    )
        public
        view
        returns (uint256[] memory playerIds, uint256[] memory quantities)
    {
        if (limit == 0) {
            return (new uint256[](0), new uint256[](0));
        }
        // Single pass: skip `offset` holdings, then collect up to `limit`
        playerIds = new uint256[](limit);
        quantities = new uint256[](limit);
        uint256 found = 0;
        uint256 added = 0;
        for (uint256 i = 0; i < nextPlayerId && added < limit; i++) {
            uint256 balance = sharesBalance[i][user];
            if (balance > 0) {
                if (found >= offset) {
                    playerIds[added] = i;
                    quantities[added] = balance;
                    added++;
                }
                found++;
            }
        }
        // Trim arrays if we found fewer than limit
        if (added < limit) {
            assembly {
                mstore(playerIds, added)
                mstore(quantities, added)
            }
        }
        return (playerIds, quantities);
    }

    /// @notice Gets a user's portfolio holdings in lots with pagination
    /// @param user The user address
    /// @param offset The starting index (in terms of holdings found, not player IDs)
    /// @param limit The maximum number of holdings to return
    /// @return playerIds Array of player IDs where user has holdings
    /// @return quantities Array of quantities in lots
    function getPortfolioHoldingsInLots(
        address user,
        uint256 offset,
        uint256 limit
    )
        public
        view
        returns (uint256[] memory playerIds, uint256[] memory quantities)
    {
        if (limit == 0) {
            return (new uint256[](0), new uint256[](0));
        }
        // Single pass: skip `offset` holdings, then collect up to `limit`
        playerIds = new uint256[](limit);
        quantities = new uint256[](limit);
        uint256 found = 0;
        uint256 added = 0;
        for (uint256 i = 0; i < nextPlayerId && added < limit; i++) {
            uint256 balance = sharesBalance[i][user];
            if (balance > 0) {
                if (found >= offset) {
                    playerIds[added] = i;
                    quantities[added] = balance / LOT_SIZE_UNITS;
                    added++;
                }
                found++;
            }
        }
        // Trim arrays if we found fewer than limit
        if (added < limit) {
            assembly {
                mstore(playerIds, added)
                mstore(quantities, added)
            }
        }
        return (playerIds, quantities);
    }

    /// @notice Gets a user's fractional remainders with pagination
    /// @param user The user address
    /// @param offset The starting index (in terms of holdings with remainders found, not player IDs)
    /// @param limit The maximum number of holdings to return
    /// @return playerIds Array of player IDs where user has fractional remainders
    /// @return remainders Array of fractional remainder amounts in units
    function getPortfolioFractionalRemainders(
        address user,
        uint256 offset,
        uint256 limit
    )
        public
        view
        returns (uint256[] memory playerIds, uint256[] memory remainders)
    {
        if (limit == 0) {
            return (new uint256[](0), new uint256[](0));
        }
        // Single pass: skip `offset` holdings with remainders, then collect up to `limit`
        playerIds = new uint256[](limit);
        remainders = new uint256[](limit);
        uint256 found = 0;
        uint256 added = 0;
        for (uint256 i = 0; i < nextPlayerId && added < limit; i++) {
            uint256 remainder = sharesBalance[i][user] % LOT_SIZE_UNITS;
            if (remainder > 0) {
                if (found >= offset) {
                    playerIds[added] = i;
                    remainders[added] = remainder;
                    added++;
                }
                found++;
            }
        }
        // Trim arrays if we found fewer than limit
        if (added < limit) {
            assembly {
                mstore(playerIds, added)
                mstore(remainders, added)
            }
        }
        return (playerIds, remainders);
    }

    /// @notice Gets the fractional remainder for a user's balance that doesn't align with LOT_SIZE_UNITS
    /// @param user The user address
    /// @param playerId The player ID
    /// @return remainder The non-lot-aligned remainder in units
    /// @return estimatedGrossPayout Estimated gross payout before fees
    /// @return estimatedNetPayout Estimated net payout after fees
    function getFractionalRemainder(
        address user,
        uint256 playerId
    ) external view playerExists(playerId) returns (
        uint256 remainder,
        uint256 estimatedGrossPayout,
        uint256 estimatedNetPayout
    ) {
        uint256 balance = sharesBalance[playerId][user];
        remainder = balance % LOT_SIZE_UNITS;

        if (remainder > 0 && sharesSupply[playerId] >= remainder) {
            estimatedGrossPayout = calculateGrossSellPayoutByUnits(playerId, remainder);
            uint256 totalFees = (estimatedGrossPayout * COMMISSION_BPS) / BPS;
            estimatedNetPayout = estimatedGrossPayout - totalFees;
        }
    }

    ////////////////////////////////////////////////////////////////////
    // Storage Cleanup - Functions
    ////////////////////////////////////////////////////////////////////

    /// @notice Cleans up sharesBalance storage slots where balance is zero
    /// @dev Idempotent - safe to call multiple times on same targets.
    ///      Off-chain indexers should deduplicate events by [playerId, user].
    ///      This function cannot distinguish between:
    ///      - Ghost data (user traded, now has 0)
    ///      - Already cleaned slots
    ///      - Never-traded slots
    ///      All three cases will emit events. Deduplication must happen off-chain.
    ///      Invalid player IDs are skipped rather than causing a revert.
    /// @param targets Array of [playerId, user] pairs to clean up
    /// @return processed Number of zero-balance targets processed
    /// @return skippedInvalidPlayer Number skipped because player doesn't exist
    /// @return skippedNonZeroBalance Number skipped because balance is not zero
    function batchCleanupZeroBalances(
        CleanupTarget[] calldata targets
    ) external onlyStorageCleaner returns (
        uint256 processed,
        uint256 skippedInvalidPlayer,
        uint256 skippedNonZeroBalance
    ) {
        require(targets.length > 0, "Empty targets array");

        for (uint256 i = 0; i < targets.length; i++) {
            uint256 playerId = targets[i].playerId;
            address user = targets[i].user;

            // Skip if player doesn't exist
            if (playerId >= nextPlayerId) {
                skippedInvalidPlayer++;
                continue;
            }

            // Skip if balance is not zero (user has active holding)
            if (sharesBalance[playerId][user] != 0) {
                skippedNonZeroBalance++;
                continue;
            }

            // Delete the storage slot
            delete sharesBalance[playerId][user];

            emit ZeroBalanceCleaned(playerId, user);
            processed++;
        }

        emit BatchCleanupCompleted("ZeroBalances", processed);
        return (processed, skippedInvalidPlayer, skippedNonZeroBalance);
    }

    /// @notice Cleans up IPO tracking data for completed IPOs
    /// @dev Only processes slots where:
    ///      - Player exists
    ///      - IPO has ended (ipoWindowEndTimestamp > 0 && block.timestamp > ipoWindowEndTimestamp)
    ///      - At least one of lotsUsed or tradesUsed is non-zero
    ///      This function IS properly idempotent - already-cleaned slots are skipped.
    ///      Invalid player IDs are skipped rather than causing a revert.
    /// @param targets Array of [playerId, user] pairs to clean up
    /// @return processed Number of slots successfully cleaned
    /// @return skippedInvalidPlayer Number skipped because player doesn't exist
    /// @return skippedActiveIpo Number skipped because IPO still active or not started
    /// @return skippedAlreadyClean Number skipped because already zero
    function batchCleanupIpoTracking(
        CleanupTarget[] calldata targets
    ) external onlyStorageCleaner returns (
        uint256 processed,
        uint256 skippedInvalidPlayer,
        uint256 skippedActiveIpo,
        uint256 skippedAlreadyClean
    ) {
        require(targets.length > 0, "Empty targets array");

        for (uint256 i = 0; i < targets.length; i++) {
            uint256 playerId = targets[i].playerId;
            address user = targets[i].user;

            // Skip if player doesn't exist
            if (playerId >= nextPlayerId) {
                skippedInvalidPlayer++;
                continue;
            }

            // Skip if IPO hasn't started or is still active
            uint256 ipoEnd = players[playerId].ipoWindowEndTimestamp;
            if (ipoEnd == 0 || block.timestamp <= ipoEnd) {
                skippedActiveIpo++;
                continue;
            }

            // Read current values
            uint256 lotsUsed = _ipoLotsUsed[playerId][user];
            uint256 tradesUsed = _ipoTradesUsed[playerId][user];

            // Skip if both values are already zero (nothing to clean)
            if (lotsUsed == 0 && tradesUsed == 0) {
                skippedAlreadyClean++;
                continue;
            }

            // Delete the storage slots
            delete _ipoLotsUsed[playerId][user];
            delete _ipoTradesUsed[playerId][user];

            emit IpoTrackingCleaned(playerId, user, lotsUsed, tradesUsed);
            processed++;
        }

        emit BatchCleanupCompleted("IpoTracking", processed);
        return (processed, skippedInvalidPlayer, skippedActiveIpo, skippedAlreadyClean);
    }

    ////////////////////////////////////////////////////////////////////
    // Internal helper functions
    ////////////////////////////////////////////////////////////////////
    function _emitUserSharesChanged(
        address user,
        uint256 playerId,
        int256 amountChange,
        uint256 totalCost,
        uint256 totalFees,
        string memory reason
    ) internal {
        uint256 netAmount = 0;
        require(amountChange != 0, "Amount change cannot be zero");
        if (amountChange != 0) {
            netAmount = amountChange > 0 ? totalCost : totalCost - totalFees;
        }
        emit UserSharesChanged(
            user,
            playerId,
            amountChange,
            sharesBalance[playerId][user],
            totalCost,
            totalFees,
            netAmount,
            reason
        );
    }

    function _emitTransferSharesChanged(
        address from,
        address to,
        uint256 playerId,
        uint256 amount,
        string memory reason
    ) internal {
        _emitUserSharesChanged(from, playerId, -int256(amount), 0, 0, reason);
        _emitUserSharesChanged(to, playerId, int256(amount), 0, 0, reason);
    }

    function _emitPlayerSupplyChanged(
        uint256 playerId,
        int256 supplyChange,
        string memory reason
    ) internal {
        uint256 s = sharesSupply[playerId];
        uint256 currentPrice = s > 0
            ? Math.mulDiv(
                _square(s),
                1 ether,
                BONDING_CURVE_DENOMINATOR * ONE_FULL_SHARE_IN_UNITS2
            )
            : 0;
        uint256 marketCap = Math.mulDiv(
            currentPrice,
            s,
            ONE_FULL_SHARE_IN_UNITS
        ); // exact with fractions
        emit PlayerSharesSupplyChanged(
            playerId,
            supplyChange,
            s,
            currentPrice,
            marketCap,
            reason
        );
    }

    /// @notice Disables the renounceOwnership function to prevent accidental ownership loss
    function renounceOwnership() public override onlyOwner {
        revert("renounceOwnership is disabled");
    }
}
Chain explorer3251msChain node89ms