Skip to content

Commit 1bd2b5a

Browse files
committed
feat(premigration): add flat grace period to all expiries
Replace the conditional --min-expiry-days padding (which only extended short v1 expiries up to a minimum) with a flat --grace-period-days (default 90) that is unconditionally added on top of every migrated name's v1 expiry. Preserves relative ordering of expiries while giving all migrated names uniform extra breathing room post-migration.
1 parent fca6a15 commit 1bd2b5a

4 files changed

Lines changed: 40 additions & 67 deletions

File tree

contracts/docs/premigration.md

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ bun run script/preMigration.ts [options]
4848
| `--limit <number>` | none | Maximum total names to process |
4949
| `--dry-run` | `false` | Simulate without sending transactions |
5050
| `--continue` | `false` | Resume from the last checkpoint |
51-
| `--min-expiry-days <days>` | `7` | Pad v2 expiry so no reserved name expires within this many days. Names whose v1 expiry falls within the window are reserved on v2 with the expiry extended up to `now + minExpiryDays`. |
51+
| `--grace-period-days <days>` | `90` | Days of grace period added on top of each name's v1 expiry. Every reserved name gets `v1Expiry + gracePeriodDays` as its v2 expiry. Set to `0` to preserve v1 expiries exactly. |
5252
| `--v1-base-registrar <address>` | `0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85` | v1 BaseRegistrar address for expiry lookups |
5353

5454
## CSV Format
@@ -85,8 +85,7 @@ The script handles quoted fields and escaped quotes within CSV values.
8585
- If already **registered** (status 2): fail — name is fully owned on v2
8686
- If already **reserved** (status 1): mark for potential renewal
8787
- If not registered or expired on v1: skip
88-
- If v1 expiry is within `--min-expiry-days`: pad the v2 expiry up to `now + min-expiry-days` and add to the batch
89-
- Otherwise: add to the batch reservation list with the v1 expiry
88+
- Add to the batch reservation list with v2 expiry = `v1Expiry + --grace-period-days`
9089
9. **Estimate gas** for the batch and preemptively split if estimated gas exceeds 80% of the block gas limit
9190
10. **Submit batch transaction** via `BatchRegistrar.batchRegister()`. If a batch reverts, recursively split it in half (binary-search fallback) until individual failing names are isolated.
9291
11. **Save checkpoint** after each batch
@@ -101,7 +100,6 @@ The script handles quoted fields and escaped quotes within CSV values.
101100
| Reserved (1) | Registered with same expiry | **Skip** (already up-to-date) |
102101
| Registered (2) | Any | **Fail** (already fully registered) |
103102
| Any | Expired or never registered | **Skip** |
104-
| Available (0) | Expiring within `min-expiry-days` | **Reserve** on v2 with padded expiry (`now + min-expiry-days`) |
105103

106104
### On-Chain Registration Parameters
107105

@@ -110,7 +108,7 @@ Each name is reserved with:
110108
- **registry**: `address(0)`
111109
- **resolver**: The ENSV1Resolver address (for fallback resolution to v1 records)
112110
- **roleBitmap**: `0`
113-
- **expires**: The v1 expiry timestamp
111+
- **expires**: The v1 expiry timestamp plus `--grace-period-days`
114112

115113
## Batch Processing
116114

@@ -173,7 +171,7 @@ Dry run still:
173171
- Reads and parses the CSV
174172
- Checks v2 state for each name
175173
- Verifies v1 registration and expiry
176-
- Applies `--min-expiry-days` padding
174+
- Applies `--grace-period-days` to each expiry
177175
- Logs what would happen
178176
- Saves checkpoints
179177

@@ -218,7 +216,6 @@ Success rate: 99%
218216
|---|---|
219217
| Invalid/empty label in CSV | Filtered out before processing, counted as `invalidLabelCount` |
220218
| Name not registered on v1 | Skipped, counted as `skippedCount` |
221-
| Name expiring within `min-expiry-days` | Reserved on v2 with expiry padded to `now + min-expiry-days` |
222219
| Name already fully registered on v2 | Counted as failure |
223220
| Batch transaction reverts | Binary-search split: recursively halves the batch until individual failures are isolated |
224221
| Batch gas estimate exceeds 80% of block limit | Preemptively splits the batch before submitting |
@@ -277,13 +274,13 @@ bun run script/preMigration.ts \
277274
bun run script/preMigration.ts --continue [same options]
278275
```
279276

280-
### Pad short expiries to at least 30 days
277+
### Custom grace period
281278

282279
```bash
283-
bun run script/preMigration.ts --min-expiry-days 30 [other options]
280+
bun run script/preMigration.ts --grace-period-days 180 [other options]
284281
```
285282

286-
Any name whose v1 expiry is within 30 days of now is still reserved on v2, but with its v2 expiry extended to `now + 30 days`.
283+
Every reserved name's v2 expiry is set to `v1Expiry + 180 days`. Pass `0` to preserve v1 expiries exactly.
287284

288285
### Custom v1 BaseRegistrar (for testing)
289286

contracts/script/preMigration.ts

Lines changed: 10 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export interface PreMigrationConfig {
109109
dryRun: boolean;
110110
continue?: boolean;
111111
disableCheckpoint?: boolean;
112-
minExpiryDays: number;
112+
gracePeriodDays: number;
113113
v1ResolverAddress: Address;
114114
v1BaseRegistrarAddress: Address;
115115
}
@@ -296,17 +296,6 @@ class PreMigrationLogger extends Logger {
296296
);
297297
}
298298

299-
paddingExpiry(
300-
name: string,
301-
originalDays: number,
302-
paddedDays: number,
303-
): void {
304-
this.raw(
305-
cyan(` → ⇪ Padding expiry: ${bold(name)}.eth`) +
306-
dim(` (${originalDays}d → ${paddedDays}d)`),
307-
` → ⇪ Padding expiry: ${name}.eth (${originalDays}d → ${paddedDays}d)`,
308-
);
309-
}
310299
}
311300

312301
const logger = new PreMigrationLogger();
@@ -853,9 +842,7 @@ async function processBatch(
853842
const alreadyReservedNames = new Set<string>();
854843
let lastLineNumber = checkpoint.lastProcessedLineNumber;
855844

856-
const minExpiryThreshold = BigInt(
857-
Math.floor(Date.now() / 1000) + config.minExpiryDays * 86400,
858-
);
845+
const gracePeriodSeconds = BigInt(config.gracePeriodDays) * 86400n;
859846

860847
const verificationResults = await batchVerifyRegistrations(
861848
registrations,
@@ -912,17 +899,7 @@ async function processBatch(
912899
continue;
913900
}
914901

915-
let effectiveExpiry = result.v1Expiry;
916-
if (result.v1Expiry <= minExpiryThreshold) {
917-
const nowSeconds = BigInt(Math.floor(Date.now() / 1000));
918-
const originalDays = Number((result.v1Expiry - nowSeconds) / 86400n);
919-
effectiveExpiry = minExpiryThreshold;
920-
logger.paddingExpiry(
921-
registration.labelName,
922-
originalDays,
923-
config.minExpiryDays,
924-
);
925-
}
902+
const effectiveExpiry = result.v1Expiry + gracePeriodSeconds;
926903

927904
const expiryDateFormatted = new Date(Number(effectiveExpiry) * 1000)
928905
.toISOString()
@@ -1096,9 +1073,9 @@ export async function main(argv = process.argv): Promise<void> {
10961073
false,
10971074
)
10981075
.option(
1099-
"--min-expiry-days <days>",
1100-
"Pad v2 expiry so no reserved name expires within this many days",
1101-
"7",
1076+
"--grace-period-days <days>",
1077+
"Days of grace period to add on top of each name's v1 expiry",
1078+
"90",
11021079
)
11031080
.requiredOption(
11041081
"--v1-resolver <address>",
@@ -1134,9 +1111,9 @@ export async function main(argv = process.argv): Promise<void> {
11341111
limit: opts.limit ? parseInt(opts.limit) : null,
11351112
dryRun: opts.dryRun,
11361113
continue: opts.continue,
1137-
minExpiryDays: Number.isNaN(parseInt(opts.minExpiryDays))
1138-
? 7
1139-
: parseInt(opts.minExpiryDays),
1114+
gracePeriodDays: Number.isNaN(parseInt(opts.gracePeriodDays))
1115+
? 90
1116+
: parseInt(opts.gracePeriodDays),
11401117
v1ResolverAddress: opts.v1Resolver as Address,
11411118
v1BaseRegistrarAddress: opts.v1BaseRegistrar as Address,
11421119
};
@@ -1156,7 +1133,7 @@ export async function main(argv = process.argv): Promise<void> {
11561133
logger.config("Mainnet RPC (v1)", config.mainnetRpcUrl);
11571134
logger.config("CSV File", config.csvFilePath);
11581135
logger.config("Batch Size", config.batchSize);
1159-
logger.config("Min Expiry Days", config.minExpiryDays);
1136+
logger.config("Grace Period Days", config.gracePeriodDays);
11601137
logger.config("V1 Resolver", config.v1ResolverAddress);
11611138
logger.config("Limit", config.limit ?? "none");
11621139
logger.config("Dry Run", config.dryRun);

contracts/test/e2e/preMigration.test.ts

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -195,48 +195,47 @@ describe("PreMigration", () => {
195195
expect(state3.status).toBe(STATUS.AVAILABLE);
196196
});
197197

198-
it("pads expiries shorter than minExpiryDays", async () => {
198+
it("adds grace period to short v1 expiries", async () => {
199199
const label = "soonexpire";
200200
const { user } = env.namedAccounts;
201201

202202
const fiveDays = 5 * 24 * 60 * 60;
203-
await registerV1Name(env, label, user.address, fiveDays);
203+
const v1Expiry = await registerV1Name(
204+
env,
205+
label,
206+
user.address,
207+
fiveDays,
208+
);
204209

205210
createCSVFile(csvFilePath, [label]);
206-
const minExpiryDays = 7;
207-
const nowSeconds = BigInt(Math.floor(Date.now() / 1000));
208-
const args = buildMainArgs(env, csvFilePath, { minExpiryDays });
211+
const gracePeriodDays = 90;
212+
const args = buildMainArgs(env, csvFilePath, { gracePeriodDays });
209213
await main(args);
210214

211215
const state = await verifyV2State(env, label);
212216
expect(state.status).toBe(STATUS.RESERVED);
213-
214-
const expectedMin =
215-
nowSeconds + BigInt(minExpiryDays) * 86400n - 120n;
216-
const expectedMax =
217-
BigInt(Math.floor(Date.now() / 1000)) +
218-
BigInt(minExpiryDays) * 86400n +
219-
120n;
220-
expect(state.expiry).toBeGreaterThanOrEqual(expectedMin);
221-
expect(state.expiry).toBeLessThanOrEqual(expectedMax);
217+
expect(state.expiry).toBe(v1Expiry + BigInt(gracePeriodDays) * 86400n);
222218
});
223219

224-
it("preserves v1 expiry when above minExpiryDays threshold", async () => {
220+
it("adds grace period to long v1 expiries", async () => {
225221
const label = "longexpire";
226222
const { user } = env.namedAccounts;
227223

228-
await registerV1Name(env, label, user.address, ONE_YEAR_SECONDS);
224+
const v1Expiry = await registerV1Name(
225+
env,
226+
label,
227+
user.address,
228+
ONE_YEAR_SECONDS,
229+
);
229230

230231
createCSVFile(csvFilePath, [label]);
231-
const args = buildMainArgs(env, csvFilePath, { minExpiryDays: 7 });
232+
const gracePeriodDays = 90;
233+
const args = buildMainArgs(env, csvFilePath, { gracePeriodDays });
232234
await main(args);
233235

234236
const state = await verifyV2State(env, label);
235237
expect(state.status).toBe(STATUS.RESERVED);
236-
237-
const paddedCeiling =
238-
BigInt(Math.floor(Date.now() / 1000)) + 30n * 86400n;
239-
expect(state.expiry).toBeGreaterThan(paddedCeiling);
238+
expect(state.expiry).toBe(v1Expiry + BigInt(gracePeriodDays) * 86400n);
240239
});
241240

242241
it("handles checkpoint resumption", async () => {

contracts/test/utils/mockPreMigration.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function buildMainArgs(
5858
dryRun?: boolean;
5959
limit?: number;
6060
continue?: boolean;
61-
minExpiryDays?: number;
61+
gracePeriodDays?: number;
6262
batchSize?: number;
6363
useEnvVarForPrivateKey?: boolean;
6464
omitPrivateKey?: boolean;
@@ -82,8 +82,8 @@ export function buildMainArgs(
8282
env.v2.ENSV1Resolver.address,
8383
"--mainnet-rpc-url",
8484
rpcUrl,
85-
"--min-expiry-days",
86-
String(overrides.minExpiryDays ?? 0),
85+
"--grace-period-days",
86+
String(overrides.gracePeriodDays ?? 0),
8787
"--v1-base-registrar",
8888
env.v1.BaseRegistrar.address,
8989
];

0 commit comments

Comments
 (0)