Raid

0xa0bc6bd46096066e617796fc8ae92ec088362401

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
10,221 bytes
ABI entries
73
35 read · 14 write
License
none

Contract information

Address
0xa0bc6bd46096066e617796fc8ae92ec088362401
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x8bdabf4df2…1d6d574ebe

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (35)

MAX_RAID_CUTOFF()uint256
MAX_SIGNATURE_TTL()uint256
MIN_TOTAL_SHARES_PER_ROUND()uint256
STUCK_RAID_TIMEOUT()uint256
WEIGHT_SCALE()uint256
canRaidNow(address)bool
domainSeparator()bytes32
eip712Domain()bytes1, string, string, uint256, address, bytes32, uint256[]
getLastResult(address)tuple
getRaidRequest(uint256)tuple
guardian()address
lastRaidRound(address)uint256
lastRaidRoundOf(address)uint256
lastResult(address)uint256, bool, uint256, uint256, uint256, bool
maxSharesPerPlayerPerRound()uint256
maxTotalSharesPerRound()uint256
nextRequestId()uint256
owner()address
paused()bool
pendingOwner()address
pendingRaid(address)bool
pendingRequestId(address)uint256
raidCutoffSeconds()uint256
raidRequests(uint256)address, uint8, uint8, uint256, bool, bool, uint256
raidWindowOpen()bool
renounceOwnership()
shareWeightByLevel(uint8)uint256
shareWeightFor(uint8)uint256
sharesAwardedThisRound(uint256, address)uint256
shipStats()address
successByChanceLevel(uint8)uint16
successChanceFor(uint8)uint16
totalSharesThisRound(uint256)uint256
treasury()address
trustedSigner()address

Events (13)

EIP712DomainChangedGuardianChangedLevelParamChangedOwnershipTransferStartedOwnershipTransferredParamChangedPausedRaidAbandonedRaidResolvedRaidStartedSharesExpiredTrustedSignerChangedUnpaused

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "shipStats_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "trustedSigner_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "initialOwner",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "ECDSAInvalidSignature",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "length",
        "type": "uint256"
      }
    ],
    "name": "ECDSAInvalidSignatureLength",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "s",
        "type": "bytes32"
      }
    ],
    "name": "ECDSAInvalidSignatureS",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EnforcedPause",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExpectedPause",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidShortString",
    "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"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "str",
        "type": "string"
      }
    ],
    "name": "StringTooLong",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [],
    "name": "EIP712DomainChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "guardian",
        "type": "address"
      }
    ],
    "name": "GuardianChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "level",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "LevelParamChanged",
    "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": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "ParamChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Paused",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "player",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "requestId",
        "type": "uint256"
      }
    ],
    "name": "RaidAbandoned",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "requestId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "player",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "success",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "sharesAwarded",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "randomValue",
        "type": "uint256"
      }
    ],
    "name": "RaidResolved",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "requestId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "player",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "chanceLevel",
        "type": "uint8"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "shareLevel",
        "type": "uint8"
      }
    ],
    "name": "RaidStarted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "player",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "round",
        "type": "uint256"
      }
    ],
    "name": "SharesExpired",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "signer",
        "type": "address"
      }
    ],
    "name": "TrustedSignerChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Unpaused",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "MAX_RAID_CUTOFF",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_SIGNATURE_TTL",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_TOTAL_SHARES_PER_ROUND",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "STUCK_RAID_TIMEOUT",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "WEIGHT_SCALE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "player",
        "type": "address"
      }
    ],
    "name": "canRaidNow",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "clearStuckRaid",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "domainSeparator",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "eip712Domain",
    "outputs": [
      {
        "internalType": "bytes1",
        "name": "fields",
        "type": "bytes1"
      },
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "version",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "chainId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "verifyingContract",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      },
      {
        "internalType": "uint256[]",
        "name": "extensions",
        "type": "uint256[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "player",
        "type": "address"
      }
    ],
    "name": "getLastResult",
    "outputs": [
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "requestId",
            "type": "uint256"
          },
          {
            "internalType": "bool",
            "name": "success",
            "type": "bool"
          },
          {
            "internalType": "uint256",
            "name": "round",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "timestamp",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "sharesAwarded",
            "type": "uint256"
          },
          {
            "internalType": "bool",
            "name": "exists",
            "type": "bool"
          }
        ],
        "internalType": "struct Raid.RaidResult",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "requestId",
        "type": "uint256"
      }
    ],
    "name": "getRaidRequest",
    "outputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "player",
            "type": "address"
          },
          {
            "internalType": "uint8",
            "name": "chanceLevel",
            "type": "uint8"
          },
          {
            "internalType": "uint8",
            "name": "shareLevel",
            "type": "uint8"
          },
          {
            "internalType": "uint256",
            "name": "round",
            "type": "uint256"
          },
          {
            "internalType": "bool",
            "name": "fulfilled",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "exists",
            "type": "bool"
          },
          {
            "internalType": "uint256",
            "name": "startedAt",
            "type": "uint256"
          }
        ],
        "internalType": "struct Raid.RaidRequest",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "guardian",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "lastRaidRound",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "player",
        "type": "address"
      }
    ],
    "name": "lastRaidRoundOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "lastResult",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "requestId",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "success",
        "type": "bool"
      },
      {
        "internalType": "uint256",
        "name": "round",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "timestamp",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "sharesAwarded",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "exists",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "maxSharesPerPlayerPerRound",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "maxTotalSharesPerRound",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "nextRequestId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "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": "address",
        "name": "player",
        "type": "address"
      }
    ],
    "name": "pendingRaid",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "pendingRequestId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "raidCutoffSeconds",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "raidRequests",
    "outputs": [
      {
        "internalType": "address",
        "name": "player",
        "type": "address"
      },
      {
        "internalType": "uint8",
        "name": "chanceLevel",
        "type": "uint8"
      },
      {
        "internalType": "uint8",
        "name": "shareLevel",
        "type": "uint8"
      },
      {
        "internalType": "uint256",
        "name": "round",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "fulfilled",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "exists",
        "type": "bool"
      },
      {
        "internalType": "uint256",
        "name": "startedAt",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "raidWindowOpen",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "requestId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "randomValue",
        "type": "uint256"
      },
      {
        "internalType": "uint64",
        "name": "deadline",
        "type": "uint64"
      },
      {
        "internalType": "bytes",
        "name": "signature",
        "type": "bytes"
      }
    ],
    "name": "resolveRaid",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "guardian_",
        "type": "address"
      }
    ],
    "name": "setGuardian",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "v",
        "type": "uint256"
      }
    ],
    "name": "setMaxSharesPerPlayerPerRound",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "v",
        "type": "uint256"
      }
    ],
    "name": "setMaxTotalSharesPerRound",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "v",
        "type": "uint256"
      }
    ],
    "name": "setRaidCutoffSeconds",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "level",
        "type": "uint8"
      },
      {
        "internalType": "uint256",
        "name": "weightScaled",
        "type": "uint256"
      }
    ],
    "name": "setShareWeight",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "level",
        "type": "uint8"
      },
      {
        "internalType": "uint16",
        "name": "percent",
        "type": "uint16"
      }
    ],
    "name": "setSuccessChance",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "signer_",
        "type": "address"
      }
    ],
    "name": "setTrustedSigner",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "name": "shareWeightByLevel",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "level",
        "type": "uint8"
      }
    ],
    "name": "shareWeightFor",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "sharesAwardedThisRound",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "shipStats",
    "outputs": [
      {
        "internalType": "contract IShipStats",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "startRaid",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "requestId",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "name": "successByChanceLevel",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "level",
        "type": "uint8"
      }
    ],
    "name": "successChanceFor",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "totalSharesThisRound",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "contract ITreasury",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "trustedSigner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "unpause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

// =============================================================================
//  ARGONAUT — Raid.sol  (РЕЙДЫ НА ПИРАМИДЫ)
// -----------------------------------------------------------------------------
//  Игрок отправляет корабль в рейд на пирамиду. Исход (успех/провал) решает
//  оффчейн-рандом с СЕРВЕРНОЙ ПОДПИСЬЮ (commit → resolve, EIP-712/ECDSA) —
//  целевая сеть Robinhood Chain (chainId 4663), где Chainlink VRF отсутствует.
//
//  ФЛОУ:
//    1) COMMIT: startRaid() снимает снапшот уровней/раунда, присваивает
//       монотонный requestId и эмитит RaidStarted. Экономика/лимиты
//       расходуются здесь (раунд занят через lastRaidRound).
//    2) OFF-CHAIN: доверенный сервер (trustedSigner) видит RaidStarted,
//       детерминированно генерит randomValue и подписывает EIP-712 пакет.
//    3) RESOLVE: resolveRaid(requestId, randomValue, deadline, signature) —
//       PERMISSIONLESS (подать может кто угодно). Контракт восстанавливает
//       подписанта через ECDSA, сверяет с trustedSigner, считает исход и при
//       успехе начисляет доли в раунд СТАРТА рейда (M1).
//
//  Уровни прокачки корабля читаются через IShipStats (реализует Ships.sol;
//  для тестов — MockShipStats).
//
//  ВЛАДЕНИЕ: OZ Ownable2Step (двухшаговая передача). Раньше owner()/onlyOwner
//  приходили из Chainlink ConfirmedOwner (через VRFConsumerBaseV2Plus); после
//  удаления VRF единственная база владения — Ownable2Step. renounceOwnership
//  переопределён реверт'ом (нельзя случайно забрикать контракт).
//
//  Держит доступ к казне (начисление долей) — требует ревью перед mainnet.
// =============================================================================

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {ITreasury} from "./interfaces/ITreasury.sol";
import {IShipStats} from "./interfaces/IShipStats.sol";

contract Raid is Ownable2Step, EIP712, Pausable, ReentrancyGuard {
    /// @notice Масштаб хранения дробных весов доли (в конфиге 1.0 => 100).
    uint256 public constant WEIGHT_SCALE = 100;

    // --------------------------------------------------------------------- //
    //  Внешние зависимости — IMMUTABLE (owner не может перенаправить поток).
    // --------------------------------------------------------------------- //
    ITreasury public immutable treasury;
    IShipStats public immutable shipStats;

    // --------------------------------------------------------------------- //
    //  Серверная подпись (EIP-712) вместо VRF
    // --------------------------------------------------------------------- //

    /// @notice Адрес доверенного подписанта результатов рейдов (серверный ключ).
    ///         onlyOwner-настраиваемый; ротация мгновенно инвалидирует старые
    ///         подписи (recover != новый signer).
    address public trustedSigner;

    /// @notice Монотонный счётчик заявок. Старт с 1 (первый requestId = 1),
    ///         поэтому 0 остаётся sentinel'ом «нет активной заявки» в
    ///         pendingRequestId. Заменяет VRF requestId.
    uint256 public nextRequestId;

    /// @dev EIP-712 typehash пакета, который подписывает сервер. Домен
    ///      (name/version/chainId/verifyingContract) вносит chainId и адрес
    ///      контракта → защита от кросс-чейн и кросс-контракт replay.
    bytes32 private constant RAID_RESOLVE_TYPEHASH =
        keccak256("RaidResolve(uint256 requestId,address player,uint256 randomValue,uint64 deadline)");

    /// @notice Верхняя граница срока годности подписи (M2). Даже если сервер
    ///         подпишет deadline на годы вперёд, resolve его не примет: deadline
    ///         не может быть дальше block.timestamp + MAX_SIGNATURE_TTL. Серверный
    ///         SIGNATURE_TTL (1 час) укладывается с огромным запасом.
    uint256 public constant MAX_SIGNATURE_TTL = 1 days;

    // --------------------------------------------------------------------- //
    //  Лимиты-страховка (на случай компрометации ключа подписанта)
    // --------------------------------------------------------------------- //

    /// @notice Глобальный кэп по СУММЕ начисленных долей за раунд (M1). Смысл —
    ///         ограничить УЩЕРБ при компрометации ключа подписанта (максимум
    ///         долей, которые можно наминтить в раунд), а НЕ число игроков.
    ///         Поражения лимит НЕ расходуют (см. _settle). Единицы веса x100.
    ///         Дефолт 100_000_000 = ~111k побед макс-уровня (L5=900) или ~1M базовых
    ///         (L0=100) за раунд — на порядки выше ожидаемой активности; owner
    ///         поднимает при росте игры. Нижняя граница защищает от «забрикать».
    uint256 public maxTotalSharesPerRound = 100_000_000;
    uint256 public constant MIN_TOTAL_SHARES_PER_ROUND = 10_000;

    /// @notice Кэп начисленных долей на игрока за раунд (единицы веса, x100;
    ///         L5 = 900, так что дефолт 10_000 = запас на defense-in-depth).
    uint256 public maxSharesPerPlayerPerRound = 10_000;

    /// @notice Сумма начисленных долей за раунд старта (для maxTotalSharesPerRound).
    mapping(uint256 => uint256) public totalSharesThisRound;
    /// @notice Начислено долей игроку в раунде (для maxSharesPerPlayerPerRound).
    mapping(uint256 => mapping(address => uint256)) public sharesAwardedThisRound;

    /// @notice «Хранитель» — может ТОЛЬКО pause() (не unpause), для мгновенной
    ///         остановки при подозрении на компрометацию, без ожидания
    ///         двухшаговой смены owner. onlyOwner-настраиваемый.
    address public guardian;

    // --------------------------------------------------------------------- //
    //  Игровые параметры (onlyOwner; начальные из gameConfig.json)
    // --------------------------------------------------------------------- //

    /// @notice Шанс успеха (в процентах) по уровню ветки «шанс».
    mapping(uint8 => uint16) public successByChanceLevel;
    /// @notice Вес доли (масштабированный x100) по уровню ветки «доля».
    mapping(uint8 => uint256) public shareWeightByLevel;

    // Лимит рейдов — НЕ временной кулдаун, а привязка к раунду казны: ровно один
    // рейд на раунд (см. lastRaidRound и startRaid). Новый раунд → право у всех
    // снова. Отдельного таймера/границ кулдауна больше нет.

    /// @notice Таймаут, после которого нерезолвленный рейд считается «зависшим»
    ///         и игрок может сам его отменить (сервер в норме отвечает за секунды).
    uint256 public constant STUCK_RAID_TIMEOUT = 1 hours;

    /// @notice «Окно тишины» в конце раунда: за raidCutoffSeconds до конца раунда
    ///         новые рейды НЕ стартуют — чтобы стартовавший рейд гарантированно успел
    ///         зарезолвиться до startNewRound (иначе сработает страховка SharesExpired
    ///         — победа без долей у границы, M1). Окно — ПРЕДОТВРАЩЕНИЕ; SharesExpired
    ///         остаётся страховкой на случай медленного резолва.
    ///         onlyOwner-настраиваемый (0..MAX_RAID_CUTOFF). Дефолт 20с — тест-безопасный
    ///         (НЕ прод-значение); в проде owner поднимает до ~120-300с.
    uint256 public raidCutoffSeconds = 20;
    /// @notice Верхняя граница сеттера окна тишины (1 час).
    uint256 public constant MAX_RAID_CUTOFF = 1 hours;

    // --------------------------------------------------------------------- //
    //  Состояние рейдов
    // --------------------------------------------------------------------- //

    /// @notice Снимок запроса рейда (фиксируется в момент старта).
    struct RaidRequest {
        address player;
        uint8 chanceLevel;
        uint8 shareLevel;
        uint256 round; // раунд казны на момент старта (для отображения)
        bool fulfilled;
        bool exists;
        uint256 startedAt; // время старта — только для таймаута clearStuckRaid
    }

    /// @notice Результат последнего рейда игрока (для фронта).
    struct RaidResult {
        uint256 requestId;
        bool success;
        uint256 round;
        uint256 timestamp;
        uint256 sharesAwarded;
        bool exists;
    }

    mapping(uint256 => RaidRequest) public raidRequests; // requestId => запрос
    mapping(address => RaidResult) public lastResult;    // player => результат
    /// @notice Номер раунда казны, в котором игрок последний раз рейдил (0 — ещё
    ///         не рейдил). Основа лимита «один рейд на раунд» (см. startRaid).
    mapping(address => uint256) public lastRaidRound;
    mapping(address => uint256) public pendingRequestId; // player => активный requestId (0 = нет)

    // --------------------------------------------------------------------- //
    //  События
    // --------------------------------------------------------------------- //
    event ParamChanged(string name, uint256 value);
    event LevelParamChanged(string name, uint8 level, uint256 value);
    event TrustedSignerChanged(address indexed signer);
    event GuardianChanged(address indexed guardian);
    event RaidStarted(uint256 indexed requestId, address indexed player, uint8 chanceLevel, uint8 shareLevel);
    /// @notice randomValue включён для ПРОВЕРЯЕМОЙ ЧЕСТНОСТИ: игроки могут
    ///         пересчитать исход из раскрытого сервером seed (commit-reveal).
    event RaidResolved(
        uint256 indexed requestId,
        address indexed player,
        bool success,
        uint256 sharesAwarded,
        uint256 randomValue
    );
    event RaidAbandoned(address indexed player, uint256 indexed requestId);
    /// @notice Победа «сгорела»: резолв пришёл уже после смены раунда, доли в
    ///         раунд старта (пул зафиксирован) начислить нельзя (M1).
    event SharesExpired(address indexed player, uint256 indexed round);

    // --------------------------------------------------------------------- //
    //  Конструктор
    // --------------------------------------------------------------------- //
    constructor(
        address treasury_,
        address shipStats_,
        address trustedSigner_,
        address initialOwner
    ) Ownable(initialOwner) EIP712("ArgonautRaid", "1") {
        require(treasury_ != address(0) && shipStats_ != address(0), "Raid: zero dependency");
        require(trustedSigner_ != address(0), "Raid: zero signer");
        treasury = ITreasury(treasury_);
        shipStats = IShipStats(shipStats_);
        trustedSigner = trustedSigner_;

        // Начальные значения из gameConfig.json (меняются на лету сеттерами).
        successByChanceLevel[0] = 20;
        successByChanceLevel[1] = 28;
        successByChanceLevel[2] = 36;
        successByChanceLevel[3] = 44;
        successByChanceLevel[4] = 50;
        successByChanceLevel[5] = 60;

        shareWeightByLevel[0] = 100; // 1.0 * 100
        shareWeightByLevel[1] = 160; // 1.6
        shareWeightByLevel[2] = 260; // 2.6
        shareWeightByLevel[3] = 400; // 4.0
        shareWeightByLevel[4] = 600; // 6.0
        shareWeightByLevel[5] = 900; // 9.0
    }

    // --------------------------------------------------------------------- //
    //  Сеттеры подписанта / хранителя / лимитов (onlyOwner)
    // --------------------------------------------------------------------- //

    /// @notice Сменить доверенного подписанта (ротация ключа). Non-zero.
    function setTrustedSigner(address signer_) external onlyOwner {
        require(signer_ != address(0), "Raid: zero signer");
        trustedSigner = signer_;
        emit TrustedSignerChanged(signer_);
    }

    /// @notice Назначить «хранителя» (может только pause()). address(0) — снять.
    function setGuardian(address guardian_) external onlyOwner {
        guardian = guardian_;
        emit GuardianChanged(guardian_);
    }

    /// @notice Глобальный кэп по сумме долей за раунд (>= MIN_TOTAL_SHARES_PER_ROUND).
    function setMaxTotalSharesPerRound(uint256 v) external onlyOwner {
        require(v >= MIN_TOTAL_SHARES_PER_ROUND, "Raid: total cap too low");
        maxTotalSharesPerRound = v;
        emit ParamChanged("maxTotalSharesPerRound", v);
    }

    /// @notice Кэп долей на игрока за раунд (>=1, единицы веса x100).
    function setMaxSharesPerPlayerPerRound(uint256 v) external onlyOwner {
        require(v >= 1, "Raid: cap>=1");
        maxSharesPerPlayerPerRound = v;
        emit ParamChanged("maxSharesPerPlayerPerRound", v);
    }

    // --------------------------------------------------------------------- //
    //  Сеттеры игровых параметров (onlyOwner)
    // --------------------------------------------------------------------- //
    function setSuccessChance(uint8 level, uint16 percent) external onlyOwner {
        require(percent <= 100, "Raid: percent>100");
        successByChanceLevel[level] = percent;
        emit LevelParamChanged("successByChanceLevel", level, percent);
    }

    function setShareWeight(uint8 level, uint256 weightScaled) external onlyOwner {
        shareWeightByLevel[level] = weightScaled;
        emit LevelParamChanged("shareWeightByLevel", level, weightScaled);
    }

    /// @notice Задать «окно тишины» перед концом раунда (0..MAX_RAID_CUTOFF).
    function setRaidCutoffSeconds(uint256 v) external onlyOwner {
        require(v <= MAX_RAID_CUTOFF, "Raid: cutoff out of range");
        raidCutoffSeconds = v;
        emit ParamChanged("raidCutoffSeconds", v);
    }

    // --------------------------------------------------------------------- //
    //  Пауза (owner ИЛИ guardian на pause; unpause только owner)
    // --------------------------------------------------------------------- //
    function pause() external {
        require(msg.sender == owner() || msg.sender == guardian, "Raid: not authorized");
        _pause();
    }

    function unpause() external onlyOwner {
        _unpause();
    }

    // --------------------------------------------------------------------- //
    //  Владение: renounceOwnership отключён (нельзя забрикать контракт)
    // --------------------------------------------------------------------- //
    function renounceOwnership() public view override onlyOwner {
        revert("Raid: renounce disabled");
    }

    // --------------------------------------------------------------------- //
    //  Флоу рейда
    // --------------------------------------------------------------------- //

    /// @notice COMMIT рейда: проверяет корабль, окно тишины и лимит «1 рейд в
    ///         раунд», снимает снимок уровней/раунда, присваивает requestId и
    ///         эмитит RaidStarted. Результат придёт отдельной tx resolveRaid,
    ///         когда сервер подпишет исход.
    function startRaid() external whenNotPaused nonReentrant returns (uint256 requestId) {
        (uint8 chanceLevel, uint8 shareLevel, bool hasShip) = shipStats.getPlayerRaidStats(msg.sender);
        require(hasShip, "Raid: no ship");

        // Окно тишины: не стартуем рейд за raidCutoffSeconds до конца раунда, чтобы
        // резолв успел до startNewRound (иначе — SharesExpired). Фронт читает то же
        // через raidWindowOpen() и показывает предупреждение вместо реверта.
        require(treasury.timeUntilNextRound() > raidCutoffSeconds, "Raid: round ending soon");

        // Один рейд на раунд: читаем текущий раунд казны и требуем, чтобы игрок
        // в нём ещё не рейдил. Новый раунд (startNewRound) → право у всех снова.
        uint256 round = treasury.getCurrentRound();
        require(lastRaidRound[msg.sender] < round, "Raid: already raided this round");
        require(pendingRequestId[msg.sender] == 0, "Raid: pending raid");

        lastRaidRound[msg.sender] = round;

        // Присваиваем монотонный requestId (вместо VRF-запроса). Сервер слушает
        // RaidStarted и подписывает исход именно для этого requestId.
        requestId = ++nextRequestId;

        // Снимок уровней и раунда в момент старта (поздний резолв использует их).
        raidRequests[requestId] = RaidRequest({
            player: msg.sender,
            chanceLevel: chanceLevel,
            shareLevel: shareLevel,
            round: round,
            fulfilled: false,
            exists: true,
            startedAt: block.timestamp
        });
        pendingRequestId[msg.sender] = requestId;

        emit RaidStarted(requestId, msg.sender, chanceLevel, shareLevel);
    }

    /// @notice RESOLVE рейда серверной подписью. PERMISSIONLESS: подать может кто
    ///         угодно (игрок, релеер, сервер) — исход зафиксирован подписью
    ///         trustedSigner и не зависит от отправителя. Идемпотентно защищён
    ///         флагом fulfilled (второй резолв ревертит).
    /// @param requestId   id заявки из RaidStarted.
    /// @param randomValue случайное значение, сгенерённое и подписанное сервером.
    /// @param deadline    срок годности подписи (unix-секунды).
    /// @param signature   EIP-712 подпись пакета RaidResolve ключом trustedSigner.
    function resolveRaid(
        uint256 requestId,
        uint256 randomValue,
        uint64 deadline,
        bytes calldata signature
    ) external whenNotPaused nonReentrant {
        require(block.timestamp <= deadline, "Raid: sig expired");
        require(deadline <= block.timestamp + MAX_SIGNATURE_TTL, "Raid: deadline too far"); // M2

        RaidRequest storage req = raidRequests[requestId];
        require(req.exists && !req.fulfilled, "Raid: bad request");

        // Проверка подписи (вынесено — иначе stack too deep). player берётся из
        // зафиксированного ончейн req.player: подпись валидна только если сервер
        // подписал именно этого игрока (это и есть «req.player == player»).
        _verifySignature(requestId, req.player, randomValue, deadline, signature);

        req.fulfilled = true;

        // Честный исход: roll в [0,99], успех если roll < шанс успеха уровня.
        bool success = (randomValue % 100) < successByChanceLevel[req.chanceLevel];
        uint256 sharesAwarded = _settle(req, success);

        lastResult[req.player] = RaidResult({
            requestId: requestId,
            success: success,
            round: req.round,
            timestamp: block.timestamp,
            sharesAwarded: sharesAwarded,
            exists: true
        });
        pendingRequestId[req.player] = 0;

        emit RaidResolved(requestId, req.player, success, sharesAwarded, randomValue);
    }

    /// @dev Проверка EIP-712 подписи резолва ключом trustedSigner. Домен
    ///      (chainId + verifyingContract) закрывает кросс-чейн/кросс-контракт replay.
    function _verifySignature(
        uint256 requestId,
        address player,
        uint256 randomValue,
        uint64 deadline,
        bytes calldata signature
    ) private view {
        bytes32 structHash = keccak256(
            abi.encode(RAID_RESOLVE_TYPEHASH, requestId, player, randomValue, deadline)
        );
        address rec = ECDSA.recover(_hashTypedDataV4(structHash), signature);
        require(rec != address(0) && rec == trustedSigner, "Raid: bad signer");
    }

    /// @dev Начисление долей при победе. Доли идут в раунд СТАРТА рейда (M1); если
    ///      раунд сменился — победа «сгорает» (SharesExpired). Лимиты расходуются
    ///      ТОЛЬКО при начислении долей — поражения кэпы не трогают (M1).
    function _settle(RaidRequest storage req, bool success) private returns (uint256) {
        if (!success) {
            return 0; // поражение — лимиты долей не расходуются
        }
        if (req.round != treasury.getCurrentRound()) {
            emit SharesExpired(req.player, req.round);
            return 0;
        }
        uint256 weight = shareWeightByLevel[req.shareLevel];

        // Кэп на игрока за раунд.
        uint256 accPlayer = sharesAwardedThisRound[req.round][req.player] + weight;
        require(accPlayer <= maxSharesPerPlayerPerRound, "Raid: player share cap");

        // Глобальный кэп по СУММЕ долей за раунд (M1) — ограничивает ущерб при
        // краже ключа, не расходуется поражениями.
        uint256 accTotal = totalSharesThisRound[req.round] + weight;
        require(accTotal <= maxTotalSharesPerRound, "Raid: round shares cap");

        sharesAwardedThisRound[req.round][req.player] = accPlayer;
        totalSharesThisRound[req.round] = accTotal;
        treasury.addSharesForRound(req.round, req.player, weight);
        return weight;
    }

    /// @notice Разблокировать «зависший» рейд, если сервер не зарезолвил за
    ///         STUCK_RAID_TIMEOUT. PERMISSIONLESS: игрок отменяет ТОЛЬКО свой
    ///         рейд. Награда НЕ начисляется; запрос помечается обработанным,
    ///         поэтому поздний resolveRaid будет отклонён. Защита доступности.
    function clearStuckRaid() external {
        uint256 reqId = pendingRequestId[msg.sender];
        require(reqId != 0, "Raid: no pending raid");
        RaidRequest storage req = raidRequests[reqId];
        require(!req.fulfilled, "Raid: already fulfilled");
        require(
            block.timestamp >= req.startedAt + STUCK_RAID_TIMEOUT,
            "Raid: not stuck yet"
        );
        req.fulfilled = true; // отменяем — поздний resolveRaid будет отклонён
        pendingRequestId[msg.sender] = 0;
        emit RaidAbandoned(msg.sender, reqId);
    }

    // --------------------------------------------------------------------- //
    //  View-функции для фронта/бэкенда
    // --------------------------------------------------------------------- //

    function getRaidRequest(uint256 requestId) external view returns (RaidRequest memory) {
        return raidRequests[requestId];
    }

    function getLastResult(address player) external view returns (RaidResult memory) {
        return lastResult[player];
    }

    /// @notice Может ли игрок рейдить прямо сейчас — true, если в текущем раунде
    ///         казны он ещё не рейдил (лимит «один рейд на раунд»). Фронт строит
    ///         таймер по timeUntilNextRound() казны, отдельного кулдауна нет.
    function canRaidNow(address player) external view returns (bool) {
        return lastRaidRound[player] < treasury.getCurrentRound();
    }

    /// @notice Номер раунда, в котором игрок последний раз рейдил (0 — не рейдил).
    function lastRaidRoundOf(address player) external view returns (uint256) {
        return lastRaidRound[player];
    }

    /// @notice Открыто ли окно рейдов прямо сейчас — false, если начался «период
    ///         тишины» перед концом раунда (осталось <= raidCutoffSeconds).
    function raidWindowOpen() external view returns (bool) {
        return treasury.timeUntilNextRound() > raidCutoffSeconds;
    }

    function successChanceFor(uint8 level) external view returns (uint16) {
        return successByChanceLevel[level];
    }

    function shareWeightFor(uint8 level) external view returns (uint256) {
        return shareWeightByLevel[level];
    }

    /// @notice Есть ли у игрока незавершённый запрос («плывём, ждём результат»).
    function pendingRaid(address player) external view returns (bool) {
        return pendingRequestId[player] != 0;
    }

    /// @notice EIP-712 domain separator — сервер использует его для подписи.
    function domainSeparator() external view returns (bytes32) {
        return _domainSeparatorV4();
    }
}
Chain explorer2523msChain node76ms