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:
- Achievement Types — admin-defined templates with stable keys, display names, metadata URIs, and configuration flags
- Issuance — recording that a player earned a specific achievement
- 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
| Mode | Requires Signature | Use Case |
|---|---|---|
| Trustless | No — caller must be owner or authorized issuer | Automated on-chain issuance by game systems |
| Server-Gated | Yes — EIP-712 from authorized signer | Off-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:
- Player reaches milestone (tracked in game server)
- Server checks conditions (XP, quest completion, match stats)
- Server signs EIP-712 payload:
IssueAchievement(player, typeId, issueRef, nonce, deadline) - Player (or any relayer) submits the signature on-chain
- 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
);
| Field | Description |
|---|---|
key | Stable string identifier — unique across all types, set once |
name | Human-readable display name |
metadataURI | External metadata (IPFS, HTTPS) |
mode | TRUSTLESS (0) or SERVER_GATED (1) |
revocable | Whether this achievement can be revoked after issuance |
active | Whether 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
| Property | Implementation |
|---|---|
| Replay protection | Per-player nonce mapping — each signature usable exactly once |
| Deadline enforcement | Server-gated signatures expire after a configurable deadline |
| Domain separation | EIP-712 typed data bound to chain ID and contract address |
| Access control | OwnableUpgradeable + issuer allowlist |
| Emergency stop | PausableUpgradeable — all mutations blocked when paused |
| Reentrancy guard | ReentrancyGuardUpgradeable on all state-changing functions |
| Upgrade safety | UUPS proxy with _disableInitializers() in constructor |
Gas Cost Model
| Operation | Gas | Cost @ 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
- Define achievement types — what should players be recognized for?
- Choose issuance mode — trustless (on-chain issuer) or server-gated (EIP-712)?
- Register types on-chain — owner calls
registerAchievementType()for each - Grant issuer role —
setIssuer(address, true)for automated issuers - Configure signer —
setAuthorizedSigner(address)for server-gated mode - Issue achievements — direct, batch, or signature-based
- Query state —
hasAchievement(),getAchievementRecord(),isRevoked() - Optional: gate access — use achievement ownership to unlock game features