Ships
0x03166761eb6fd0bc6cb8660d00789f605cdf05cf
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
16,512 bytes
ABI entries
108
50 read · 18 write
License
none
Contract information
- Address
- 0x03166761eb6fd0bc6cb8660d00789f605cdf05cf
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0xC48Fd800E8…52E6839cF0
- Creation tx
- 0x7495e78132…e71f88f6b5
Token
- Name
- —
- Symbol
- —
- Decimals
- —
- Holders
- 11
- Market
- View token page →
Read contract (50)
BRANCH_CHANCE() → uint8
BRANCH_SHARE() → uint8
CRAFT_SUCCESS_L1() → uint16
CRAFT_SUCCESS_L2() → uint16
CRAFT_SUCCESS_L3() → uint16
CRAFT_SUCCESS_L4() → uint16
MAX_CRAFT_PRICE() → uint256
MAX_ITEM_PRICE() → uint256
MAX_LEVEL() → uint8
MAX_SHIP_PRICE() → uint256
MAX_SIGNATURE_TTL() → uint256
MIN_CRAFT_PRICE() → uint256
MIN_ITEM_PRICE() → uint256
MIN_SHIP_PRICE() → uint256
PRICE_TIMELOCK() → uint256
SHIP_ID() → uint256
STUCK_CRAFT_TIMEOUT() → uint256
balanceOf(address, uint256) → uint256
balanceOfBatch(address[], uint256[]) → uint256[]
craftRequestedAt(address) → uint256
craftRequests(uint256) → address, uint8, uint8, bool, bool
craftSuccessFor(uint8) → uint16
domainSeparator() → bytes32
eip712Domain() → bytes1, string, string, uint256, address, bytes32, uint256[]
equippedChanceLevel(address) → uint8
equippedShareLevel(address) → uint8
getEquipped(address) → uint8, uint8
getLastCraft(address) → tuple
getPlayerRaidStats(address) → uint8, uint8, bool
guardian() → address
hasShip(address) → bool
isApprovedForAll(address, address) → bool
itemBalance(address, uint8, uint8) → uint256
itemPriceArg(uint8) → uint256
lastCraft(address) → uint256, uint8, uint8, bool, bool
nextRequestId() → uint256
owner() → address
paused() → bool
pendingCraft(address) → bool
pendingCraftId(address) → uint256
pendingItemPrice(uint8) → uint256, uint256
pendingOwner() → address
pendingShipPrice() → uint256, uint256
renounceOwnership()
shipPriceArg() → uint256
supportsInterface(bytes4) → bool
token() → address
treasury() → address
trustedSigner() → address
uri(uint256) → string
Events (18)
ApprovalForAllCraftAbandonedCraftRequestedCraftResolvedEIP712DomainChangedEquippedGuardianChangedItemBoughtOwnershipTransferStartedOwnershipTransferredPausedPriceChangeAnnouncedShipBoughtTransferBatchTransferSingleTrustedSignerChangedURIUnpaused
ABI
[
{
"inputs": [
{
"internalType": "string",
"name": "uri_",
"type": "string"
},
{
"internalType": "address",
"name": "token_",
"type": "address"
},
{
"internalType": "address",
"name": "treasury_",
"type": "address"
},
{
"internalType": "address",
"name": "trustedSigner_",
"type": "address"
},
{
"internalType": "address",
"name": "initialOwner",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "address",
"name": "target",
"type": "address"
}
],
"name": "AddressEmptyCode",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "AddressInsufficientBalance",
"type": "error"
},
{
"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": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "needed",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ERC1155InsufficientBalance",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "approver",
"type": "address"
}
],
"name": "ERC1155InvalidApprover",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "idsLength",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "valuesLength",
"type": "uint256"
}
],
"name": "ERC1155InvalidArrayLength",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "ERC1155InvalidOperator",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "receiver",
"type": "address"
}
],
"name": "ERC1155InvalidReceiver",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
}
],
"name": "ERC1155InvalidSender",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "ERC1155MissingApprovalForAll",
"type": "error"
},
{
"inputs": [],
"name": "EnforcedPause",
"type": "error"
},
{
"inputs": [],
"name": "ExpectedPause",
"type": "error"
},
{
"inputs": [],
"name": "FailedInnerCall",
"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": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [
{
"internalType": "string",
"name": "str",
"type": "string"
}
],
"name": "StringTooLong",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "ApprovalForAll",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "player",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "requestId",
"type": "uint256"
}
],
"name": "CraftAbandoned",
"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": "branch",
"type": "uint8"
},
{
"indexed": false,
"internalType": "uint8",
"name": "fromLevel",
"type": "uint8"
}
],
"name": "CraftRequested",
"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": "branch",
"type": "uint8"
},
{
"indexed": false,
"internalType": "uint8",
"name": "fromLevel",
"type": "uint8"
},
{
"indexed": false,
"internalType": "bool",
"name": "success",
"type": "bool"
},
{
"indexed": false,
"internalType": "uint256",
"name": "randomValue",
"type": "uint256"
}
],
"name": "CraftResolved",
"type": "event"
},
{
"anonymous": false,
"inputs": [],
"name": "EIP712DomainChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "player",
"type": "address"
},
{
"indexed": false,
"internalType": "uint8",
"name": "branch",
"type": "uint8"
},
{
"indexed": false,
"internalType": "uint8",
"name": "level",
"type": "uint8"
}
],
"name": "Equipped",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "guardian",
"type": "address"
}
],
"name": "GuardianChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "player",
"type": "address"
},
{
"indexed": false,
"internalType": "uint8",
"name": "branch",
"type": "uint8"
},
{
"indexed": false,
"internalType": "uint8",
"name": "level",
"type": "uint8"
},
{
"indexed": false,
"internalType": "uint256",
"name": "qty",
"type": "uint256"
}
],
"name": "ItemBought",
"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": false,
"internalType": "string",
"name": "name",
"type": "string"
},
{
"indexed": false,
"internalType": "uint8",
"name": "level",
"type": "uint8"
},
{
"indexed": false,
"internalType": "uint256",
"name": "newPrice",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "effectiveAt",
"type": "uint256"
}
],
"name": "PriceChangeAnnounced",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "player",
"type": "address"
}
],
"name": "ShipBought",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256[]",
"name": "ids",
"type": "uint256[]"
},
{
"indexed": false,
"internalType": "uint256[]",
"name": "values",
"type": "uint256[]"
}
],
"name": "TransferBatch",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "TransferSingle",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "signer",
"type": "address"
}
],
"name": "TrustedSignerChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "string",
"name": "value",
"type": "string"
},
{
"indexed": true,
"internalType": "uint256",
"name": "id",
"type": "uint256"
}
],
"name": "URI",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "Unpaused",
"type": "event"
},
{
"inputs": [],
"name": "BRANCH_CHANCE",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "BRANCH_SHARE",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "CRAFT_SUCCESS_L1",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "CRAFT_SUCCESS_L2",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "CRAFT_SUCCESS_L3",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "CRAFT_SUCCESS_L4",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_CRAFT_PRICE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_ITEM_PRICE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_LEVEL",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_SHIP_PRICE",
"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_CRAFT_PRICE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MIN_ITEM_PRICE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MIN_SHIP_PRICE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "PRICE_TIMELOCK",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "SHIP_ID",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "STUCK_CRAFT_TIMEOUT",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
},
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address[]",
"name": "accounts",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "ids",
"type": "uint256[]"
}
],
"name": "balanceOfBatch",
"outputs": [
{
"internalType": "uint256[]",
"name": "",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint8",
"name": "branch",
"type": "uint8"
},
{
"internalType": "uint8",
"name": "level",
"type": "uint8"
},
{
"internalType": "uint256",
"name": "qty",
"type": "uint256"
}
],
"name": "buyItem",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "buyShip",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "clearStuckCraft",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint8",
"name": "branch",
"type": "uint8"
},
{
"internalType": "uint8",
"name": "fromLevel",
"type": "uint8"
}
],
"name": "craft",
"outputs": [
{
"internalType": "uint256",
"name": "requestId",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "craftRequestedAt",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "craftRequests",
"outputs": [
{
"internalType": "address",
"name": "player",
"type": "address"
},
{
"internalType": "uint8",
"name": "branch",
"type": "uint8"
},
{
"internalType": "uint8",
"name": "fromLevel",
"type": "uint8"
},
{
"internalType": "bool",
"name": "fulfilled",
"type": "bool"
},
{
"internalType": "bool",
"name": "exists",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint8",
"name": "fromLevel",
"type": "uint8"
}
],
"name": "craftSuccessFor",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "pure",
"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": "uint8",
"name": "branch",
"type": "uint8"
},
{
"internalType": "uint8",
"name": "level",
"type": "uint8"
}
],
"name": "equip",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "equippedChanceLevel",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "equippedShareLevel",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "player",
"type": "address"
}
],
"name": "getEquipped",
"outputs": [
{
"internalType": "uint8",
"name": "chanceLevel",
"type": "uint8"
},
{
"internalType": "uint8",
"name": "shareLevel",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "player",
"type": "address"
}
],
"name": "getLastCraft",
"outputs": [
{
"components": [
{
"internalType": "uint256",
"name": "requestId",
"type": "uint256"
},
{
"internalType": "uint8",
"name": "branch",
"type": "uint8"
},
{
"internalType": "uint8",
"name": "fromLevel",
"type": "uint8"
},
{
"internalType": "bool",
"name": "success",
"type": "bool"
},
{
"internalType": "bool",
"name": "exists",
"type": "bool"
}
],
"internalType": "struct Ships.CraftResult",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "player",
"type": "address"
}
],
"name": "getPlayerRaidStats",
"outputs": [
{
"internalType": "uint8",
"name": "chanceLevel",
"type": "uint8"
},
{
"internalType": "uint8",
"name": "shareLevel",
"type": "uint8"
},
{
"internalType": "bool",
"name": "playerHasShip",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "guardian",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "hasShip",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "isApprovedForAll",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "player",
"type": "address"
},
{
"internalType": "uint8",
"name": "branch",
"type": "uint8"
},
{
"internalType": "uint8",
"name": "level",
"type": "uint8"
}
],
"name": "itemBalance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint8",
"name": "level",
"type": "uint8"
}
],
"name": "itemPriceArg",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "lastCraft",
"outputs": [
{
"internalType": "uint256",
"name": "requestId",
"type": "uint256"
},
{
"internalType": "uint8",
"name": "branch",
"type": "uint8"
},
{
"internalType": "uint8",
"name": "fromLevel",
"type": "uint8"
},
{
"internalType": "bool",
"name": "success",
"type": "bool"
},
{
"internalType": "bool",
"name": "exists",
"type": "bool"
}
],
"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": [
{
"internalType": "address",
"name": "player",
"type": "address"
}
],
"name": "pendingCraft",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "pendingCraftId",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint8",
"name": "level",
"type": "uint8"
}
],
"name": "pendingItemPrice",
"outputs": [
{
"internalType": "uint256",
"name": "pending",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "effectiveAt",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingShipPrice",
"outputs": [
{
"internalType": "uint256",
"name": "pending",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "effectiveAt",
"type": "uint256"
}
],
"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": "resolveCraft",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256[]",
"name": "ids",
"type": "uint256[]"
},
{
"internalType": "uint256[]",
"name": "values",
"type": "uint256[]"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "safeBatchTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "setApprovalForAll",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "guardian_",
"type": "address"
}
],
"name": "setGuardian",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint8",
"name": "level",
"type": "uint8"
},
{
"internalType": "uint256",
"name": "newPrice",
"type": "uint256"
}
],
"name": "setItemPrice",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "newPrice",
"type": "uint256"
}
],
"name": "setShipPrice",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "signer_",
"type": "address"
}
],
"name": "setTrustedSigner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "newUri",
"type": "string"
}
],
"name": "setURI",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "shipPriceArg",
"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": "token",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"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"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "uri",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
// =============================================================================
// ARGONAUT — Ships.sol (КОРАБЛИ, ПРЕДМЕТЫ, КРАФТ, УРОВНИ)
// -----------------------------------------------------------------------------
// ERC-1155: корабли и предметы игроков. Ведёт уровни двух веток прокачки
// (шанс/доля), обрабатывает покупку корабля, покупку базовых предметов и крафт
// с риском поломки. Реализует IShipStats — источник уровней для Raid.
// Все платежи в ARG уходят в Treasury.
//
// ID-СХЕМА:
// ID 0 = Корабль (Ship). В v1 у игрока максимум 1 корабль (hasShip).
// ID 1..5 = Предметы ветки ШАНС (Compass), уровни 1..5.
// ID 6..10 = Предметы ветки ДОЛЯ (Idol), уровни 1..5.
// ID 11..15 = ЗАРЕЗЕРВИРОВАНО под ветку ПЕРЕЗАРЯДКА (фаза 1.5) — НЕ используется.
//
// ЭКИПИРОВКА (v1, упрощённо): игрок держит один «активный» уровень на ветку.
// equippedChanceLevel / equippedShareLevel. Крафт более высокого уровня
// авто-экипирует его; equip() позволяет выбрать вручную.
//
// ОПЛАТА (выбранный путь): игрок делает approve этому контракту. Ships берёт
// token.safeTransferFrom(player -> Ships), делает forceApprove(Treasury) и
// зовёт treasury.deposit(amount) (стандартный путь депозита, без спец-функций
// в казне). Итог: средства сразу оказываются в Treasury, Ships не копит ARG.
//
// КРАФТ И РАНДОМ: исход крафта решает оффчейн-рандом с СЕРВЕРНОЙ ПОДПИСЬЮ
// (commit → resolve, EIP-712/ECDSA) — зеркально Raid.sol; целевая сеть
// Robinhood Chain (chainId 4663), где Chainlink VRF отсутствует. craft()
// СРАЗУ сжигает 2 предмета и берёт плату в казну, присваивает монотонный
// requestId и эмитит CraftRequested. Исход приходит отдельной tx
// resolveCraft, когда сервер (trustedSigner) подпишет результат. Это
// закрывает эксплойт «simulate-then-revert»: в момент commit-tx результат
// неизвестен, а предметы уже сгорели, откатить неудачу и повторить нельзя.
//
// ВЛАДЕНИЕ: OZ Ownable2Step (двухшаговая передача). Раньше owner()/onlyOwner
// приходили из Chainlink ConfirmedOwner (через VRFConsumerBaseV2Plus); после
// удаления VRF единственная база владения — Ownable2Step. renounceOwnership
// переопределён реверт'ом (нельзя случайно забрикать контракт).
//
// Держит игровые NFT и маршрутизирует средства — требует ревью перед mainnet.
// =============================================================================
import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
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 Ships is ERC1155, Ownable2Step, EIP712, Pausable, ReentrancyGuard, IShipStats {
using SafeERC20 for IERC20;
// --------------------------------------------------------------------- //
// Константы ID / веток
// --------------------------------------------------------------------- //
uint256 public constant SHIP_ID = 0;
uint8 public constant BRANCH_CHANCE = 0; // Compass, ID 1..5
uint8 public constant BRANCH_SHARE = 1; // Idol, ID 6..10
uint8 public constant MAX_LEVEL = 5;
// --------------------------------------------------------------------- //
// Внешние зависимости — IMMUTABLE (owner не может подменить/перенаправить).
// --------------------------------------------------------------------- //
IERC20 public immutable token;
ITreasury public immutable treasury;
// --------------------------------------------------------------------- //
// Серверная подпись (EIP-712) вместо VRF
// --------------------------------------------------------------------- //
/// @notice Адрес доверенного подписанта результатов крафта (серверный ключ).
/// onlyOwner-настраиваемый; ротация мгновенно инвалидирует старые
/// подписи (recover != новый signer).
address public trustedSigner;
/// @notice Монотонный счётчик заявок крафта. Старт с 1 (первый requestId = 1),
/// поэтому 0 остаётся sentinel'ом «нет активной заявки» в
/// pendingCraftId. Заменяет VRF requestId.
uint256 public nextRequestId;
/// @notice «Хранитель» — может ТОЛЬКО pause() (не unpause), для мгновенной
/// остановки при подозрении на компрометацию ключа, без ожидания
/// двухшаговой смены owner. onlyOwner-настраиваемый.
address public guardian;
/// @dev EIP-712 typehash пакета, который подписывает сервер. Домен
/// ("ArgonautShips" — ОТЛИЧАЕТСЯ от Raid для разделения подписей) вносит
/// chainId и адрес контракта → защита от кросс-чейн/кросс-контракт replay.
bytes32 private constant CRAFT_RESOLVE_TYPEHASH =
keccak256("CraftResolve(uint256 requestId,address player,uint256 randomValue,uint64 deadline)");
/// @notice Верхняя граница срока годности подписи (M2). Resolve не примет
/// deadline дальше block.timestamp + MAX_SIGNATURE_TTL — даже если
/// сервер подпишет его на годы вперёд. Серверный SIGNATURE_TTL (1 час)
/// укладывается с огромным запасом.
uint256 public constant MAX_SIGNATURE_TTL = 1 days;
// --------------------------------------------------------------------- //
// Цены (в ARG, 18 decimals) с ТАЙМЛОКОМ 1ч и жёсткими границами
// --------------------------------------------------------------------- //
/// @notice Таймлок на изменение цен: новая цена вступает в силу только через
/// 1 час после объявления (защита игрока от мгновенного повышения).
uint256 public constant PRICE_TIMELOCK = 1 hours;
// Границы цен (в ARG, 18 decimals). Сеттеры не могут выйти за них.
uint256 public constant MIN_SHIP_PRICE = 20_000 ether;
uint256 public constant MAX_SHIP_PRICE = 2_000_000 ether;
uint256 public constant MIN_ITEM_PRICE = 10_000 ether;
uint256 public constant MAX_ITEM_PRICE = 1_000_000 ether;
uint256 public constant MIN_CRAFT_PRICE = 5_000 ether;
uint256 public constant MAX_CRAFT_PRICE = 15_000_000 ether;
/// @notice Цена с отложенным применением. Пока `block.timestamp < effectiveAt`
/// действует `current`; после — `pending`. effectiveAt=0 => сразу current.
struct TimedPrice {
uint256 current;
uint256 pending;
uint64 effectiveAt;
}
TimedPrice private _shipPrice;
mapping(uint8 => TimedPrice) private _itemPrices; // level(1..5)
// --------------------------------------------------------------------- //
// Шансы крафта — КОНСТАНТЫ (правила игры, изменить нельзя)
// --------------------------------------------------------------------- //
uint16 public constant CRAFT_SUCCESS_L1 = 40; // Lv1 -> 2
uint16 public constant CRAFT_SUCCESS_L2 = 30; // Lv2 -> 3
uint16 public constant CRAFT_SUCCESS_L3 = 20; // Lv3 -> 4
uint16 public constant CRAFT_SUCCESS_L4 = 15; // Lv4 -> 5
// --------------------------------------------------------------------- //
// Состояние игроков
// --------------------------------------------------------------------- //
mapping(address => bool) public hasShip;
mapping(address => uint8) public equippedChanceLevel;
mapping(address => uint8) public equippedShareLevel;
// --------------------------------------------------------------------- //
// Асинхронный крафт через VRF
// --------------------------------------------------------------------- //
/// @notice Таймаут «зависшего» крафта (сервер не зарезолвил) — игрок может сам
/// разблокироваться (предметы уже сожжены, награда НЕ выдаётся).
uint256 public constant STUCK_CRAFT_TIMEOUT = 1 hours;
struct CraftRequest {
address player;
uint8 branch;
uint8 fromLevel;
bool fulfilled;
bool exists;
}
struct CraftResult {
uint256 requestId;
uint8 branch;
uint8 fromLevel;
bool success;
bool exists;
}
mapping(uint256 => CraftRequest) public craftRequests; // requestId => запрос
mapping(address => CraftResult) public lastCraft; // player => последний результат
mapping(address => uint256) public pendingCraftId; // player => активный requestId (0 = нет)
mapping(address => uint256) public craftRequestedAt; // player => время запроса (для таймаута)
// --------------------------------------------------------------------- //
// События
// --------------------------------------------------------------------- //
event ShipBought(address indexed player);
event ItemBought(address indexed player, uint8 branch, uint8 level, uint256 qty);
event TrustedSignerChanged(address indexed signer);
event GuardianChanged(address indexed guardian);
event CraftRequested(uint256 indexed requestId, address indexed player, uint8 branch, uint8 fromLevel);
/// @notice randomValue включён для ПРОВЕРЯЕМОЙ ЧЕСТНОСТИ: игроки могут
/// пересчитать исход из раскрытого сервером seed (commit-reveal).
event CraftResolved(
uint256 indexed requestId,
address indexed player,
uint8 branch,
uint8 fromLevel,
bool success,
uint256 randomValue
);
event CraftAbandoned(address indexed player, uint256 indexed requestId);
event Equipped(address indexed player, uint8 branch, uint8 level);
/// @notice Объявлено изменение цены. Вступит в силу в `effectiveAt` (через 1ч).
event PriceChangeAnnounced(string name, uint8 level, uint256 newPrice, uint256 effectiveAt);
// --------------------------------------------------------------------- //
// Конструктор
// --------------------------------------------------------------------- //
constructor(
string memory uri_,
address token_,
address treasury_,
address trustedSigner_,
address initialOwner
) ERC1155(uri_) Ownable(initialOwner) EIP712("ArgonautShips", "1") {
require(token_ != address(0) && treasury_ != address(0), "Ships: zero dependency");
require(trustedSigner_ != address(0), "Ships: zero signer");
token = IERC20(token_);
treasury = ITreasury(treasury_);
trustedSigner = trustedSigner_;
// Стартовые цены в ARG (действуют сразу — effectiveAt=0). Владелец может
// менять их сеттерами (с таймлоком 1ч и в пределах границ).
_shipPrice.current = 200_000 ether; // корабль
_itemPrices[1].current = 100_000 ether; // покупка предмета (компас/сосуд)
_itemPrices[2].current = 70_000 ether; // крафт Lv1->2
_itemPrices[3].current = 190_000 ether; // крафт Lv2->3
_itemPrices[4].current = 500_000 ether; // крафт Lv3->4
_itemPrices[5].current = 1_300_000 ether;// крафт Lv4->5
// Шансы крафта — константы (см. CRAFT_SUCCESS_L1..L4), не задаются здесь.
}
/// @dev Шанс успеха крафта (%) по исходному уровню — из констант.
function _craftSuccess(uint8 fromLevel) internal pure returns (uint16) {
if (fromLevel == 1) return CRAFT_SUCCESS_L1;
if (fromLevel == 2) return CRAFT_SUCCESS_L2;
if (fromLevel == 3) return CRAFT_SUCCESS_L3;
if (fromLevel == 4) return CRAFT_SUCCESS_L4;
return 0;
}
// --------------------------------------------------------------------- //
// Внутреннее: ID предмета и сбор оплаты
// --------------------------------------------------------------------- //
/// @dev Вычислить ERC-1155 ID предмета по ветке и уровню (level 1..5).
function _itemId(uint8 branch, uint8 level) internal pure returns (uint256) {
require(branch <= BRANCH_SHARE, "Ships: bad branch");
require(level >= 1 && level <= MAX_LEVEL, "Ships: bad level");
// Chance: 1..5 ; Share: 6..10
return branch == BRANCH_CHANCE ? uint256(level) : uint256(5 + level);
}
/// @dev Забрать `amount` ARG у игрока и переслать в казну (deposit).
function _collect(address player, uint256 amount) internal {
if (amount == 0) return;
token.safeTransferFrom(player, address(this), amount);
token.forceApprove(address(treasury), amount);
treasury.deposit(amount);
}
/// @dev Действующая цена: pending после наступления effectiveAt, иначе current.
function _effectivePrice(TimedPrice storage p) internal view returns (uint256) {
if (p.effectiveAt != 0 && block.timestamp >= p.effectiveAt) {
return p.pending;
}
return p.current;
}
function _autoEquip(address player, uint8 branch, uint8 level) internal {
if (branch == BRANCH_CHANCE) {
if (level > equippedChanceLevel[player]) {
equippedChanceLevel[player] = level;
emit Equipped(player, branch, level);
}
} else {
if (level > equippedShareLevel[player]) {
equippedShareLevel[player] = level;
emit Equipped(player, branch, level);
}
}
}
// --------------------------------------------------------------------- //
// Покупки
// --------------------------------------------------------------------- //
/// @notice Купить корабль (в v1 — один на игрока). Оплата ARG уходит в казну.
function buyShip() external whenNotPaused nonReentrant {
require(!hasShip[msg.sender], "Ships: already has ship");
_collect(msg.sender, shipPriceArg());
hasShip[msg.sender] = true;
_mint(msg.sender, SHIP_ID, 1, "");
emit ShipBought(msg.sender);
}
/// @notice Купить базовый предмет (ТОЛЬКО уровень 1). Уровни 2..5 — крафтом.
function buyItem(uint8 branch, uint8 level, uint256 qty) external whenNotPaused nonReentrant {
require(branch <= BRANCH_SHARE, "Ships: bad branch");
require(level == 1, "Ships: craft only");
require(qty > 0, "Ships: zero qty");
_collect(msg.sender, itemPriceArg(1) * qty);
_mint(msg.sender, _itemId(branch, 1), qty, "");
emit ItemBought(msg.sender, branch, level, qty);
}
// --------------------------------------------------------------------- //
// Крафт (сердце прокачки)
// --------------------------------------------------------------------- //
/// @notice COMMIT крафта предмета уровня fromLevel+1 из двух предметов
/// fromLevel. СРАЗУ (до исхода): берёт плату в казну и сжигает 2
/// предмета — потеря необратима. Затем присваивает requestId и эмитит
/// CraftRequested; исход приходит отдельной tx resolveCraft, когда
/// сервер подпишет результат. Т.к. в момент commit результат
/// неизвестен, а предметы уже сгорели — эксплойт simulate-then-revert
/// не работает. Один активный крафт на игрока за раз.
function craft(uint8 branch, uint8 fromLevel)
external
whenNotPaused
nonReentrant
returns (uint256 requestId)
{
require(branch <= BRANCH_SHARE, "Ships: bad branch");
require(fromLevel >= 1 && fromLevel <= 4, "Ships: bad fromLevel");
require(pendingCraftId[msg.sender] == 0, "Ships: craft pending");
uint256 fromId = _itemId(branch, fromLevel);
require(balanceOf(msg.sender, fromId) >= 2, "Ships: need 2 items");
uint8 toLevel = fromLevel + 1;
// Плата за крафт = цена целевого уровня, сразу в казну.
_collect(msg.sender, itemPriceArg(toLevel));
// Сжигаем 2 предмета исходного уровня СРАЗУ (потеря необратима).
_burn(msg.sender, fromId, 2);
// Если сгорел последний экземпляр экипированного уровня — снять экипировку
// ветки уже сейчас (при успехе fulfillRandomWords авто-экипирует toLevel).
if (balanceOf(msg.sender, fromId) == 0) {
if (branch == BRANCH_CHANCE && equippedChanceLevel[msg.sender] == fromLevel) {
equippedChanceLevel[msg.sender] = 0;
emit Equipped(msg.sender, branch, 0);
} else if (branch == BRANCH_SHARE && equippedShareLevel[msg.sender] == fromLevel) {
equippedShareLevel[msg.sender] = 0;
emit Equipped(msg.sender, branch, 0);
}
}
// Присваиваем монотонный requestId (вместо VRF-запроса). Сервер слушает
// CraftRequested и подписывает исход именно для этого requestId.
requestId = ++nextRequestId;
craftRequests[requestId] = CraftRequest({
player: msg.sender,
branch: branch,
fromLevel: fromLevel,
fulfilled: false,
exists: true
});
pendingCraftId[msg.sender] = requestId;
craftRequestedAt[msg.sender] = block.timestamp;
emit CraftRequested(requestId, msg.sender, branch, fromLevel);
}
/// @notice RESOLVE крафта серверной подписью. PERMISSIONLESS: подать может кто
/// угодно (игрок, релеер, сервер) — исход зафиксирован подписью
/// trustedSigner и не зависит от отправителя. Идемпотентно защищён
/// флагом fulfilled (второй резолв ревертит). При успехе минтит предмет
/// следующего уровня и авто-экипирует.
/// @param requestId id заявки из CraftRequested.
/// @param randomValue случайное значение, сгенерённое и подписанное сервером.
/// @param deadline срок годности подписи (unix-секунды).
/// @param signature EIP-712 подпись пакета CraftResolve ключом trustedSigner.
function resolveCraft(
uint256 requestId,
uint256 randomValue,
uint64 deadline,
bytes calldata signature
) external whenNotPaused nonReentrant {
require(block.timestamp <= deadline, "Ships: sig expired");
require(deadline <= block.timestamp + MAX_SIGNATURE_TTL, "Ships: deadline too far"); // M2
CraftRequest storage req = craftRequests[requestId];
require(req.exists && !req.fulfilled, "Ships: bad request");
// Проверка подписи. player берётся из зафиксированного ончейн req.player —
// подпись валидна, только если сервер подписал именно этого игрока. Домен
// EIP-712 ("ArgonautShips") связывает chainId и адрес контракта → нет
// кросс-чейн / кросс-контракт replay (в т.ч. подпись домена Raid не пройдёт).
_verifySignature(requestId, req.player, randomValue, deadline, signature);
req.fulfilled = true;
pendingCraftId[req.player] = 0; // CEI: снимаем блокировку до внешних вызовов (mint hook)
bool success = (randomValue % 100) < _craftSuccess(req.fromLevel);
lastCraft[req.player] = CraftResult({
requestId: requestId,
branch: req.branch,
fromLevel: req.fromLevel,
success: success,
exists: true
});
if (success) {
uint8 toLevel = req.fromLevel + 1;
_mint(req.player, _itemId(req.branch, toLevel), 1, "");
_autoEquip(req.player, req.branch, toLevel);
}
emit CraftResolved(requestId, req.player, req.branch, req.fromLevel, success, 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(CRAFT_RESOLVE_TYPEHASH, requestId, player, randomValue, deadline)
);
address rec = ECDSA.recover(_hashTypedDataV4(structHash), signature);
require(rec != address(0) && rec == trustedSigner, "Ships: bad signer");
}
/// @notice Разблокировать «зависший» крафт, если сервер не зарезолвил за
/// таймаут. PERMISSIONLESS (только свой). Предмет НЕ выдаётся (2
/// предмета уже сожжены, плата в казне) — защита доступности, не возврат.
function clearStuckCraft() external {
uint256 reqId = pendingCraftId[msg.sender];
require(reqId != 0, "Ships: no pending craft");
CraftRequest storage req = craftRequests[reqId];
require(!req.fulfilled, "Ships: already fulfilled");
require(
block.timestamp >= craftRequestedAt[msg.sender] + STUCK_CRAFT_TIMEOUT,
"Ships: not stuck yet"
);
req.fulfilled = true; // поздний resolveCraft будет отклонён
pendingCraftId[msg.sender] = 0;
emit CraftAbandoned(msg.sender, reqId);
}
/// @notice Вручную экипировать имеющийся предмет ветки (выбор активного уровня).
function equip(uint8 branch, uint8 level) external whenNotPaused {
require(branch <= BRANCH_SHARE, "Ships: bad branch");
require(level >= 1 && level <= MAX_LEVEL, "Ships: bad level");
require(balanceOf(msg.sender, _itemId(branch, level)) >= 1, "Ships: item not owned");
if (branch == BRANCH_CHANCE) {
equippedChanceLevel[msg.sender] = level;
} else {
equippedShareLevel[msg.sender] = level;
}
emit Equipped(msg.sender, branch, level);
}
// --------------------------------------------------------------------- //
// Сеттеры подписанта / хранителя (onlyOwner). token/treasury — immutable.
// --------------------------------------------------------------------- //
/// @notice Сменить доверенного подписанта (ротация ключа). Non-zero.
function setTrustedSigner(address signer_) external onlyOwner {
require(signer_ != address(0), "Ships: zero signer");
trustedSigner = signer_;
emit TrustedSignerChanged(signer_);
}
/// @notice Назначить «хранителя» (может только pause()). address(0) — снять.
function setGuardian(address guardian_) external onlyOwner {
guardian = guardian_;
emit GuardianChanged(guardian_);
}
// --------------------------------------------------------------------- //
// Сеттеры цен/параметров (onlyOwner)
// --------------------------------------------------------------------- //
/// @notice Объявить новую цену корабля. Вступит в силу через PRICE_TIMELOCK (1ч).
/// До этого действует прежняя (уже действующая) цена.
function setShipPrice(uint256 newPrice) external onlyOwner {
require(newPrice >= MIN_SHIP_PRICE && newPrice <= MAX_SHIP_PRICE, "Ships: ship price out of range");
// Зафиксировать текущую действующую цену как current, объявить pending.
_shipPrice.current = _effectivePrice(_shipPrice);
_shipPrice.pending = newPrice;
_shipPrice.effectiveAt = uint64(block.timestamp + PRICE_TIMELOCK);
emit PriceChangeAnnounced("ship", 0, newPrice, _shipPrice.effectiveAt);
}
/// @notice Объявить новую цену предмета/крафта уровня (таймлок 1ч).
/// level 1 = цена покупки предмета (границы предметов);
/// level 2..5 = плата за крафт целевого уровня (границы крафта).
function setItemPrice(uint8 level, uint256 newPrice) external onlyOwner {
require(level >= 1 && level <= MAX_LEVEL, "Ships: bad level");
if (level == 1) {
require(newPrice >= MIN_ITEM_PRICE && newPrice <= MAX_ITEM_PRICE, "Ships: item price out of range");
} else {
require(newPrice >= MIN_CRAFT_PRICE && newPrice <= MAX_CRAFT_PRICE, "Ships: craft price out of range");
}
TimedPrice storage tp = _itemPrices[level];
tp.current = _effectivePrice(tp);
tp.pending = newPrice;
tp.effectiveAt = uint64(block.timestamp + PRICE_TIMELOCK);
emit PriceChangeAnnounced("item", level, newPrice, tp.effectiveAt);
}
function setURI(string calldata newUri) external onlyOwner {
_setURI(newUri);
}
// Пауза: owner ИЛИ guardian на pause; unpause только owner.
function pause() external {
require(msg.sender == owner() || msg.sender == guardian, "Ships: not authorized");
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
// Владение: renounceOwnership отключён (нельзя забрикать контракт).
function renounceOwnership() public view override onlyOwner {
revert("Ships: renounce disabled");
}
// --------------------------------------------------------------------- //
// View
// --------------------------------------------------------------------- //
/// @inheritdoc IShipStats
function getPlayerRaidStats(address player)
external
view
override
returns (uint8 chanceLevel, uint8 shareLevel, bool playerHasShip)
{
return (equippedChanceLevel[player], equippedShareLevel[player], hasShip[player]);
}
function getEquipped(address player) external view returns (uint8 chanceLevel, uint8 shareLevel) {
return (equippedChanceLevel[player], equippedShareLevel[player]);
}
function itemBalance(address player, uint8 branch, uint8 level) external view returns (uint256) {
return balanceOf(player, _itemId(branch, level));
}
/// @notice Шанс успеха крафта (%) по исходному уровню — из констант. Фронт читает это.
function craftSuccessFor(uint8 fromLevel) external pure returns (uint16) {
return _craftSuccess(fromLevel);
}
/// @notice Действующая цена корабля (учитывает таймлок). Фронт читает её.
function shipPriceArg() public view returns (uint256) {
return _effectivePrice(_shipPrice);
}
/// @notice Действующая цена предмета/крафта уровня (учитывает таймлок).
function itemPriceArg(uint8 level) public view returns (uint256) {
return _effectivePrice(_itemPrices[level]);
}
/// @notice Информация об анонсированном изменении цены (для UI/прозрачности).
/// effectiveAt=0 или уже наступил => изменения в очереди нет.
function pendingShipPrice() external view returns (uint256 pending, uint256 effectiveAt) {
return (_shipPrice.pending, _shipPrice.effectiveAt);
}
function pendingItemPrice(uint8 level) external view returns (uint256 pending, uint256 effectiveAt) {
return (_itemPrices[level].pending, _itemPrices[level].effectiveAt);
}
/// @notice Есть ли у игрока незавершённый крафт (ждём подпись сервера).
function pendingCraft(address player) external view returns (bool) {
return pendingCraftId[player] != 0;
}
/// @notice Последний результат крафта игрока (для фронта).
function getLastCraft(address player) external view returns (CraftResult memory) {
return lastCraft[player];
}
/// @notice EIP-712 domain separator — сервер использует его для подписи.
function domainSeparator() external view returns (bytes32) {
return _domainSeparatorV4();
}
/// @dev Разрешаем конфликт наследования supportsInterface. Только ERC1155
/// реализует ERC165 в цепочке (у Ownable/EIP712 его нет) → override(ERC1155).
function supportsInterface(bytes4 interfaceId) public view override(ERC1155) returns (bool) {
return super.supportsInterface(interfaceId);
}
}
Chain explorer3621msChain node92ms