๐ชช Profile Registry & QuizScoresV2
Profiles (name + X username) used to live inside each event's QuizScores contract. That meant a participant had a separate profile per event, and nothing outside the quiz could use it.
Now there is one shared ProfileRegistry.sol, used by:
QuizScoresV2.sol: one per event, stores scores onlyFeedContract.sol: submissions and votes on the Feed
The three earlier events keep their v1 QuizScores contracts and leaderboards unchanged. They are marked legacy: true in src/config/events.js.
How the pieces fitโ
โโโโโโโโโโโโโโโโโโโโโโโโ
โ ProfileRegistry โ name + X username per wallet
โโโโโโโโโโโโฒโโโโโโโโโโโโ
isRegistered / โ getProfilesBatch
getProfile โ
โโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโ
โ โ โ
โโโโโโโดโโโโโโโโโ โโโโโโโโโโดโโโโโโโ โโโโโโโโโโโดโโโโโโ
โ QuizScoresV2 โ โ QuizScoresV2 โ โ FeedContract โ
โ (event N) โ โ (event N+1) โ โ โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
Legacy (read-only from the portal's point of view):
QuizScores v1 (ATLAS) ยท QuizScores v1 (DBIT) ยท QuizScores v1 (Hacktoberfest)
ProfileRegistryโ
| Function | Who | Description |
|---|---|---|
register(name, xUsername) | Anyone | Same rules as v1: name โค 64, X username โค 32, case-insensitive unique, permanent. Only open after finalizeMigration() |
migrateFrom(legacy, start, count) | Owner, before finalize | Copies profiles from a v1 contract by reading getLeaderboardBatch on-chain |
finalizeMigration() | Owner, once | Closes migration permanently and opens registration |
isRegistered(wallet) | View | |
getProfile(wallet) | View | Reverts if not registered |
getProfilesBatch(wallets[]) | View | Names, X usernames and a registered flag for each wallet |
getProfileCount() / getProfilesPage(start, count) | View | Enumeration |
Trust modelโ
migrateFrom reads profile data directly from the legacy contract, so the owner can't insert made-up profiles. The owner only decides the order and timing. After finalizeMigration() the owner has no special powers left.
Migration rulesโ
- A wallet that is already registered is skipped (
MigrationSkipped, reason1). - A wallet whose X username is already held by another wallet is skipped (reason
2). v1 contracts only enforced uniqueness per event, so the same handle can exist across events. - Migrate the newest event first. The first write wins, so a wallet registered at several events keeps its most recent name, and a contested handle goes to its most recent claimant.
- Skipped participants still show normally on their legacy leaderboard. To use the Feed or future quizzes, they register again with a different handle.
QuizScoresV2โ
Same anti-cheat model as v1, minus profile storage:
| Change vs. v1 | Detail |
|---|---|
| Constructor | (address admin, address profileRegistry) |
| Registration | Removed. submitScore requires registry.isRegistered(msg.sender) |
isRegistered(wallet) | Pass-through to the registry (used by api/quiz/start.js) |
| Leaderboard | A wallet is listed after its first score. getLeaderboardBatch returns (wallets[], batchScores[]); names come from registry.getProfilesBatch |
getScores(wallet) | Replaces getParticipant |
| Signed message | keccak256(contract, chainId, wallet, quizId, score, nonce) |
The signed message now includes the contract address and chain ID. In v1, every event contract started nonces at 0 and the message didn't name the contract, so a signature issued for one event could be replayed on another. api/quiz/complete.js detects whether VITE_CONTRACT_ADDRESS is a v1 or v2 contract (only v2 has registry()) and signs the matching format.
Deployment runbook (Remix)โ
All contracts compile with Solidity 0.8.19+. Use Injected Provider - MetaMask on Sepolia.
1. Wait for the last legacy event to closeโ
Anyone who registers on a v1 contract after its migration batch has run won't be carried over. Run migration once Hacktoberfest (rtta1cub) registrations are closed. You can re-run migrateFrom with a later start offset at any time before finalizing.
2. Deploy and migrate the registryโ
-
Deploy
ProfileRegistry.solwith(forwarderAddress)(see Gasless Transactions;0x0000000000000000000000000000000000000000disables gasless calls). The deploying wallet becomes the owner. -
For each legacy contract, newest first, call
migrateFrom(address, start, count)in batches of ~30โ50:Order Event Contract 1 Hacktoberfest Hack Day Mumbai 0x487cb8c812e4177107884799fdbe61a2b4374c732 DBIT Mumbai 0xb54c3d5fea76dae38e615cc1ecf7024c7afc1b2d3 ATLAS SkillTech 0xb5456aDfa33BD3c94A6E021541d73Abc26A19138Use
getParticipantCount()on each legacy contract to know how many batches to run. -
Check
MigrationSkippedevents on Etherscan and comparegetProfileCount()with what you expect. -
Call
finalizeMigration(). This can't be undone.
3. Deploy the feedโ
Deploy FeedContract.sol with (registryAddress, forwarderAddress). The deploying wallet becomes the moderator (see Feed).
4. Point the portal at the new contractsโ
VITE_PROFILE_REGISTRY_ADDRESS=0xYourProfileRegistry
VITE_FEED_CONTRACT_ADDRESS=0xYourFeedContract
Once VITE_PROFILE_REGISTRY_ADDRESS is set, the wallet connects, reads profiles and registers through the registry. Without it, the portal falls back to the old behaviour (profiles on VITE_CONTRACT_ADDRESS).
Only set VITE_PROFILE_REGISTRY_ADDRESS after finalizeMigration(). Before that, new users can't register.
5. Next eventโ
- Deploy
QuizScoresV2.solwith(adminAddress, registryAddress, forwarderAddress). - Append it to
src/config/events.jswithoutlegacy: true. - Point
VITE_CONTRACT_ADDRESSat it. No API change is needed; the signing format is detected automatically.
Testing in the Remix VMโ
Run this before touching Sepolia (Remix VM environment, several accounts):
- Deploy two v1
QuizScorescontracts. Register overlapping wallets, including the same wallet on both with different names, and two wallets with the same X handle (different case) on different contracts. - Deploy
ProfileRegistry(0x0). Confirmregisterreverts with Registration not open yet. migrateFromthe newer contract, then the older one. Check that:- the shared wallet has the newer name
- the clashing handle went to the newer contract's wallet, and the other got
MigrationSkippedwith reason2 - re-running a batch changes nothing
finalizeMigration. ConfirmmigrateFromnow reverts,registerworks, and a taken handle in different case is rejected.- Deploy
QuizScoresV2(admin, registry, 0x0). Sign a score from the admin account and submit it. Deploy a second V2 and confirm the same signature is rejected there. - Deploy
FeedContract(registry, 0x0)and run the checks in Feed โ Testing.