Skip to main content

Achievements

On-chain attestations that prove a player accomplished something specific — verifiable, revocable, and permanently recorded.


Purpose

Achievements are on-chain records that prove a player accomplished something specific in gameplay. Unlike NFT minting, achievements are lightweight attestations stored directly in the AchievementsRegistry contract. They serve as:

  • Reputation markers — visible proof of skill or dedication
  • Access gates — required for certain game modes, guilds, or features
  • Historical records — permanent, verifiable gameplay accomplishments
  • Progression signals — tracked and queryable by any external system

How It Works

The AchievementsRegistry is a UUPS-upgradeable contract that manages:

  1. Achievement Types — admin-defined templates with stable keys, display names, metadata URIs, and configuration flags
  2. Issuance — recording that a player earned a specific achievement
  3. Revocation — removing an achievement if the type allows it
GAME SERVER                                     BLOCKCHAIN
═══════════ ══════════

Player reaches milestone


Server checks achievement rules

├──── TRUSTLESS ──────────────────► Issuer calls
│ (issuer has on-chain role) issueAchievement()
│ │
│ ▼
│ AchievementsRegistry
│ records achievement

└──── SERVER_GATED ──────────────► Server signs EIP-712:
(off-chain eligibility) IssueAchievement(
player, typeId,
issueRef, nonce,
deadline
)


Player (or relayer)
calls issueAchievementWithSig()


Contract verifies sig,
records achievement

Issuance Modes

ModeRequires SignatureUse Case
TrustlessNo — caller must be owner or authorized issuerAutomated on-chain issuance by game systems
Server-GatedYes — EIP-712 from authorized signerOff-chain eligibility checked by game server

Trustless Issuance

For achievements where the issuing authority is an on-chain role:

// Owner or authorized issuer calls directly
registry.issueAchievement(typeId, playerAddress, issueRef);

// Batch issuance (e.g., season-end awards)
registry.issueAchievementBatch(typeId, players, refs);

Server-Gated Issuance

For achievements where eligibility depends on off-chain game state:

  1. Player reaches milestone (tracked in game server)
  2. Server checks conditions (XP, quest completion, match stats)
  3. Server signs EIP-712 payload: IssueAchievement(player, typeId, issueRef, nonce, deadline)
  4. Player (or any relayer) submits the signature on-chain
  5. Contract verifies signature + nonce → records achievement

Replay protection: Each signature uses a per-player nonce tracked on-chain. Expired deadlines and used nonces are permanently rejected.


Achievement Types

Types are registered by the contract owner and define the template for each achievement:

registry.registerAchievementType(
"season_1_champion", // stable key (unique, immutable)
"Season 1 Champion", // display name
"ipfs://Qm.../meta.json", // metadata URI
1, // mode: 0 = TRUSTLESS, 1 = SERVER_GATED
false // revocable: false = permanent
);
FieldDescription
keyStable string identifier — unique across all types, set once
nameHuman-readable display name
metadataURIExternal metadata (IPFS, HTTPS)
modeTRUSTLESS (0) or SERVER_GATED (1)
revocableWhether this achievement can be revoked after issuance
activeWhether new issuances are accepted

Example Achievement Types

// Milestone achievements
{ key: "first_blood", name: "First Blood", condition: "wins >= 1" }
{ key: "veteran", name: "Veteran", condition: "wins >= 100" }
{ key: "legendary", name: "Legendary", condition: "wins >= 1000" }

// Season achievements
{ key: "diamond_s1", name: "Diamond Season 1", condition: "season_1_league == diamond" }
{ key: "top_10_s1", name: "Top 10 Season 1", condition: "season_1_rank <= 10" }

// Event achievements
{ key: "blitz_champ_001", name: "Blitz Champion", condition: "event_blitz_001_rank == 1" }

Revocation

Achievements marked as revocable can be revoked by the owner or an authorized issuer:

// Single revocation
registry.revokeAchievement(typeId, playerAddress, revokeRef);

// Batch revocation (e.g., ban wave)
registry.revokeAchievementBatch(typeId, players, refs);

Non-revocable achievements are permanent — once issued, they can never be removed. This is the recommended default for skill-based accomplishments.

Re-issuance: After revocation, an achievement can be re-issued to the same player. The contract tracks the full history.


Contract Interface

interface IAchievements {
// ── Type Management (Owner Only) ──
function registerAchievementType(string key, string name, string metadataURI, uint8 mode, bool revocable) external;
function updateAchievementType(uint32 typeId, string name, string metadataURI, uint8 mode, bool revocable, bool active) external;

// ── Issuance (Owner or Issuer) ──
function issueAchievement(uint32 typeId, address player, bytes32 issueRef) external;
function issueAchievementBatch(uint32 typeId, address[] players, bytes32[] issueRefs) external;
function issueAchievementWithSig(uint32 typeId, address player, bytes32 issueRef, uint256 nonce, uint256 deadline, bytes sig) external;

// ── Revocation (Owner or Issuer) ──
function revokeAchievement(uint32 typeId, address player, bytes32 revokeRef) external;
function revokeAchievementBatch(uint32 typeId, address[] players, bytes32[] revokeRefs) external;

// ── Views ──
function hasAchievement(uint32 typeId, address player) external view returns (bool);
function isRevoked(uint32 typeId, address player) external view returns (bool);
function getAchievementRecord(uint32 typeId, address player) external view returns (AchievementRecord memory);
function getAchievementType(uint32 typeId) external view returns (AchievementType memory);
function achievementTypeCount() external view returns (uint32);
function isIssuer(address account) external view returns (bool);
function isNonceUsed(address player, uint256 nonce) external view returns (bool);
}

Security Properties

PropertyImplementation
Replay protectionPer-player nonce mapping — each signature usable exactly once
Deadline enforcementServer-gated signatures expire after a configurable deadline
Domain separationEIP-712 typed data bound to chain ID and contract address
Access controlOwnableUpgradeable + issuer allowlist
Emergency stopPausableUpgradeable — all mutations blocked when paused
Reentrancy guardReentrancyGuardUpgradeable on all state-changing functions
Upgrade safetyUUPS proxy with _disableInitializers() in constructor

Gas Cost Model

OperationGasCost @ 10 gwei
Register achievement type~80k~0.0008 AVAX
Issue single achievement (direct)~65k~0.00065 AVAX
Issue with EIP-712 signature~75k~0.00075 AVAX
Batch issue (10 players)~350k~0.0035 AVAX
Revoke achievement~45k~0.00045 AVAX

Integration Guide

  1. Define achievement types — what should players be recognized for?
  2. Choose issuance mode — trustless (on-chain issuer) or server-gated (EIP-712)?
  3. Register types on-chain — owner calls registerAchievementType() for each
  4. Grant issuer rolesetIssuer(address, true) for automated issuers
  5. Configure signersetAuthorizedSigner(address) for server-gated mode
  6. Issue achievements — direct, batch, or signature-based
  7. Query statehasAchievement(), getAchievementRecord(), isRevoked()
  8. Optional: gate access — use achievement ownership to unlock game features