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
- Creator
- 0xC48Fd800E8…52E6839cF0
- 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