This guide details the migration from TheSeeds v1.0.0 to v1.1.0, which includes critical security fixes and important feature enhancements.
Issue: Users could submit duplicate token IDs to multiply their voting power.
Fix: Added duplicate detection in _verifyOwnership() function.
Impact:
- ✅ No API changes - existing blessing functions work the same
- ✅ No migration required - fix is backward compatible
⚠️ Behavior change: Blessings with duplicate token IDs will now revert withInvalidMerkleProof
Action Required:
- Frontend/Backend: Ensure token ID arrays are deduplicated before submission
- Testing: Verify your Merkle proof generation doesn't include duplicates
Issue: Integer precision loss in sqrt calculation caused unfair scoring for small blessing counts.
Fix: Scale values BEFORE sqrt using SCORE_SCALE_FACTOR = 1e6
Impact:
- ✅ No API changes
⚠️ Score values changed: All scores are now ~1000x larger (due to scaling)⚠️ Historical comparison: Old scores vs new scores are not directly comparable
Action Required:
- Analytics/UI: Update score display logic (scores are now in different magnitude)
- Database: Consider storing
versionfield with each score for historical comparison - Recommendation: Don't compare scores across contract versions
Issue: Retracted seeds were marked as isWinner = true, causing confusion.
Fix: Added separate isRetracted boolean field in Seed struct.
Impact:
⚠️ Breaking Change: Seed struct has new field⚠️ Storage layout changed: Contract is NOT upgradeable without migration
Migration Steps:
// OLD Seed struct
struct Seed {
uint256 id;
address creator;
string ipfsHash;
uint256 blessings;
uint256 createdAt;
bool isWinner;
uint256 winnerInRound;
uint256 submittedInRound;
}
// NEW Seed struct
struct Seed {
uint256 id;
address creator;
string ipfsHash;
uint256 blessings;
uint256 createdAt;
bool isWinner;
bool isRetracted; // NEW FIELD
uint256 winnerInRound;
uint256 submittedInRound;
}Action Required:
- Deployment: Deploy as NEW contract (not upgrade)
- Data Migration: If preserving state, write migration script to:
- Read all seeds from old contract
- Identify seeds where
isWinner = truebut NOT actual winners - Mark those as
isRetracted = truein new contract
- Frontend: Update seed display logic to check both
isWinnerandisRetracted
New Constants:
uint256 public constant MAX_SEEDS_PER_ROUND = 1000;
uint256 public constant MAX_TOTAL_SEEDS = 100000;Impact:
- ✅ Gas safety: Prevents unbounded array growth
⚠️ Behavior change:submitSeed()will revert after limits reached
Action Required:
- Monitoring: Track
seedCountandroundSeedIds[currentRound].length - Planning: Design for round rotation before hitting 1000 seeds/round
- Frontend: Display remaining capacity to users
Error Handling:
try {
await contract.submitSeed(ipfsHash);
} catch (error) {
if (error.message.includes('MaxSeedsReached')) {
// Handle total limit reached
} else if (error.message.includes('RoundSeedLimitReached')) {
// Handle per-round limit reached
}
}New Validation:
- CIDv0: 46 characters, starts with 'Qm'
- CIDv1: 59 characters, starts with 'b'
- General: 10-100 characters (flexible)
Impact:
⚠️ Behavior change: Invalid IPFS hashes now revert
Action Required:
- Frontend: Validate IPFS hash before submission
- Testing: Update tests to use valid IPFS hashes
// Frontend validation example
function validateIPFSHash(hash: string): boolean {
if (hash.length === 46 && hash.startsWith('Qm')) return true;
if (hash.length === 59 && hash.startsWith('b')) return true;
if (hash.length >= 10 && hash.length <= 100) return true;
return false;
}New Variables:
uint256 public nextVotingPeriod;
uint256 public nextBlessingsPerNFT;New Behavior:
updateVotingPeriod()- schedules update for next roundupdateBlessingsPerNFT()- schedules update for next round
Impact:
⚠️ Behavior change: Updates no longer take effect immediately- ✅ Safer: Prevents mid-round rule changes
Action Required:
- Admin UI: Show both current and scheduled values
- Communication: Inform users of upcoming changes
// Check for scheduled updates
const currentPeriod = await contract.votingPeriod();
const nextPeriod = await contract.nextVotingPeriod();
if (nextPeriod > 0) {
console.log(`Voting period will change from ${currentPeriod} to ${nextPeriod} next round`);
}New Variable:
bool public resetScoresOnRoundEnd;New Function:
function updateScoreResetPolicy(bool _enabled) external onlyRole(ADMIN_ROLE);Impact:
- ✅ Optional feature: Default is
false(backward compatible) - ✅ Configurable: Can be enabled/disabled anytime
Use Cases:
resetScoresOnRoundEnd = false: Scores accumulate across rounds (good for NON_ROUND_BASED mode)resetScoresOnRoundEnd = true: Fresh start each round (good for ROUND_BASED mode)
Action Required:
- Strategy decision: Choose appropriate policy for your use case
- Communication: Explain policy to users
New Events:
event SeedScoreUpdated(uint256 indexed seedId, address indexed blesser, uint256 previousScore, uint256 newScore, uint256 decayFactor);
event BlessingFailed(uint256 indexed seedId, address indexed blesser, string reason);
event VotingPeriodScheduled(uint256 currentPeriod, uint256 scheduledPeriod);
event BlessingsPerNFTScheduled(uint256 currentAmount, uint256 scheduledAmount);
event ScoreResetPolicyUpdated(bool resetScores);
event ScoresReset(uint256 indexed round, uint256 seedsAffected);Impact:
- ✅ Better observability: Track score changes in real-time
- ✅ Debugging: Identify failed blessings
- ✅ Transparency: Users see score updates
Action Required:
- Indexer: Update event listeners to handle new events
- Analytics: Use
SeedScoreUpdatedfor detailed score tracking - Monitoring: Alert on
BlessingFailedevents
// Listen to score updates
contract.on('SeedScoreUpdated', (seedId, blesser, prevScore, newScore, decayFactor) => {
console.log(`Seed ${seedId}: ${prevScore} → ${newScore} (decay: ${decayFactor})`);
});
// Listen to failed blessings
contract.on('BlessingFailed', (seedId, blesser, reason) => {
console.error(`Blessing failed for seed ${seedId} by ${blesser}: ${reason}`);
});Updated Function:
function pause(string calldata reason) external onlyRole(ADMIN_ROLE);Impact:
⚠️ Breaking Change:pause()now requires areasonparameter- ✅ Better transparency: Users see why contract is paused
Action Required:
- Admin UI: Update pause function calls to include reason
// OLD
await contract.pause();
// NEW
await contract.pause("Emergency maintenance - investigating score anomaly");Updated Constructor:
constructor(address _admin, address _initialCreator)Impact:
⚠️ Breaking Change: Constructor signature changed- ✅ Better UX: Contract is immediately usable
Action Required:
- Deployment: Pass initial creator address (can be
address(0)if not needed)
// OLD
const contract = await TheSeedsFactory.deploy(adminAddress);
// NEW
const contract = await TheSeedsFactory.deploy(adminAddress, creatorAddress);- Review all security fixes
- Update frontend to handle new error types
- Update event listeners for new events
- Test Merkle proof generation (ensure no duplicates)
- Validate IPFS hashes before submission
- Update constructor call with initial creator
- Deploy new contract (NOT upgrade)
- Grant ADMIN_ROLE to admin address
- Grant CREATOR_ROLE to authorized creators
- Grant RELAYER_ROLE to backend relayers
- Set initial Merkle root
- Configure round mode, tie-breaking, deadlock strategies
- Set score reset policy
- Verify contract on block explorer
- Test all blessing flows (direct, delegated, batch)
- Test configuration updates (voting period, blessings per NFT)
- Test winner selection
- Monitor events for anomalies
- Update documentation with new contract address
- Communicate changes to users
Deploy new contract and start fresh. This is the cleanest and safest approach.
Pros:
- No migration complexity
- No risk of data corruption
- Clean slate for new features
Cons:
- Historical data lost on-chain (can be preserved off-chain)
If you must preserve on-chain state, follow these steps:
-
Export Old State:
const oldSeeds = []; const seedCount = await oldContract.seedCount(); for (let i = 0; i < seedCount; i++) { const seed = await oldContract.getSeed(i); oldSeeds.push(seed); }
-
Identify Retracted Seeds:
const retractedSeeds = oldSeeds.filter(seed => { // Logic to identify truly retracted vs real winners // This depends on your off-chain records return seed.isWinner && !isRealWinner(seed.id); });
-
Deploy and Populate New Contract:
// Create migration function in new contract function migrateSeeds( Seed[] calldata _seeds ) external onlyRole(ADMIN_ROLE) { // Batch insert seeds }
-
Verify Migration:
// Compare old vs new for (let i = 0; i < seedCount; i++) { const oldSeed = await oldContract.getSeed(i); const newSeed = await newContract.getSeed(i); // Verify data integrity }
-
Duplicate Token ID Prevention:
it("should reject duplicate token IDs", async () => { await expect( contract.blessSeed(0, [1, 1, 2], proof) ).to.be.revertedWithCustomError(contract, "InvalidMerkleProof"); });
-
Score Precision:
it("should calculate scores with precision", async () => { // Test 1, 2, 3, 4 blessings and verify scores increase properly });
-
Retracted Seeds:
it("should exclude retracted from selection", async () => { await contract.retractSeed(0); // Verify seed 0 cannot win });
-
Array Limits:
it("should enforce MAX_SEEDS_PER_ROUND", async () => { // Submit 1000 seeds await expect(contract.submitSeed(...)).to.be.revertedWithCustomError( contract, "RoundSeedLimitReached" ); });
- Test full blessing flow with new contract
- Test winner selection with retracted seeds
- Test score reset policy
- Test deferred config updates
- Test all new events
| Function | v1.0.0 | v1.1.0 | Breaking Change |
|---|---|---|---|
constructor |
✅ | Yes - requires _initialCreator |
|
submitSeed |
✅ | ✅ Enhanced | No - but validates IPFS hash |
retractSeed |
✅ | ✅ Enhanced | No - but sets isRetracted |
blessSeed |
✅ | ✅ Enhanced | No - but rejects duplicates |
blessSeedFor |
✅ | ✅ | No |
batchBlessSeedsFor |
✅ | ✅ Enhanced | No - but emits failures |
selectDailyWinner |
✅ | ✅ Enhanced | No - but resets scores optionally |
pause |
✅ | Yes - requires reason |
|
unpause |
✅ | ✅ | No |
updateVotingPeriod |
✅ | No - but deferred to next round | |
updateBlessingsPerNFT |
✅ | No - but deferred to next round | |
getSeed |
✅ | Yes - struct has isRetracted |
|
getCurrentLeader |
✅ | ✅ Enhanced | No - but filters retracted |
getEligibleSeeds |
✅ | ✅ Enhanced | No - but filters retracted |
A: No. The storage layout changed (new isRetracted field). You must deploy a new contract.
A: No. The scale factor changed. Old scores are ~1000x smaller than new scores.
A: Yes, for:
- Constructor calls (new parameter)
- Pause function (requires reason)
- Error handling (new error types)
- Event listening (new events)
- IPFS hash validation
A: They remain marked as winners in old contract. In new contract, they should be marked as isRetracted = true.
A: Yes! All configurability is preserved and enhanced.
A: Depends on your use case:
- Round-based competition: Enable reset for fair rounds
- Cumulative competition: Disable reset to accumulate support over time
If you encounter issues during migration:
- Check this guide first
- Review test suite in
/test/TheSeedsFixed.test.ts - Review contract code for detailed NatSpec comments
- Open an issue on GitHub
Security Fixes:
- ✅ FIX: Duplicate token ID attack prevention
- ✅ FIX: Score precision improvement with scale factor
- ✅ FIX: Retracted seeds semantic clarity
New Features:
- ✅ NEW: Array growth limits (MAX_SEEDS_PER_ROUND, MAX_TOTAL_SEEDS)
- ✅ NEW: IPFS hash validation
- ✅ NEW: Deferred configuration updates
- ✅ NEW: Score reset policy (configurable)
- ✅ NEW: Enhanced events (SeedScoreUpdated, BlessingFailed, etc.)
- ✅ NEW: Pause with reason
- ✅ NEW: Initial creator in constructor
- ✅ NEW: Contract version tracking (VERSION constant)
Improvements:
- Filtered retracted seeds from all view functions
- Better gas efficiency with block scopes
- Improved documentation
- Initial release
Last Updated: 2025-12-17
Contract Version: 1.1.0
Migration Status: