Skip to content

fix(precompiles): validate V1/V2 activity state in initialize_if_migrated (ZELLIC-64)#2864

Open
howydev wants to merge 1 commit intomainfrom
howy/fix-initialize-if-migrated-activity-validation
Open

fix(precompiles): validate V1/V2 activity state in initialize_if_migrated (ZELLIC-64)#2864
howydev wants to merge 1 commit intomainfrom
howy/fix-initialize-if-migrated-activity-validation

Conversation

@howydev
Copy link
Contributor

@howydev howydev commented Feb 25, 2026

Summary

ZELLIC-64: initialize_if_migrated only checked that the validator count matched between V1 and V2, but did not verify each validator's activity state was consistent. A validator could be deactivated in V2 during migration while still active in V1 (or vice versa), leading to inconsistent state after initialization.

Changes

  • Add per-validator activity state validation in initialize_if_migrated / initializeIfMigrated:
    • V1 active == true → V2 deactivatedAtHeight must be 0
    • V1 active == false → V2 deactivatedAtHeight must be > 0
  • Add new MigrationStateMismatch(uint64 idx) error to identify which validator has mismatched state
  • Updated in both Solidity ref-impl and Rust precompile

Files changed

  • tips/ref-impls/src/interfaces/IValidatorConfigV2.sol — new error definition
  • tips/ref-impls/src/ValidatorConfigV2.sol — activity state validation loop
  • crates/contracts/src/precompiles/validator_config_v2.rs — new error in sol! macro + constructor
  • crates/precompiles/src/validator_config_v2/mod.rs — activity state validation loop

@github-actions
Copy link

github-actions bot commented Feb 25, 2026

📊 Tempo Precompiles Coverage

precompiles

Coverage: 20762/21794 lines (95.26%)

File details
File Lines Coverage
src/account_keychain/dispatch.rs 36/41 87.80%
src/account_keychain/mod.rs 1131/1150 98.35%
src/error.rs 139/158 87.97%
src/ip_validation.rs 10/10 100.00%
src/lib.rs 328/339 96.76%
src/nonce/dispatch.rs 19/23 82.61%
src/nonce/mod.rs 252/260 96.92%
src/stablecoin_dex/dispatch.rs 349/353 98.87%
src/stablecoin_dex/error.rs 51/51 100.00%
src/stablecoin_dex/mod.rs 2997/3093 96.90%
src/stablecoin_dex/order.rs 362/362 100.00%
src/stablecoin_dex/orderbook.rs 651/683 95.31%
src/storage/evm.rs 321/347 92.51%
src/storage/hashmap.rs 128/140 91.43%
src/storage/mod.rs 5/5 100.00%
src/storage/packing.rs 526/552 95.29%
src/storage/thread_local.rs 146/195 74.87%
src/storage/types/array.rs 211/262 80.53%
src/storage/types/bytes_like.rs 323/338 95.56%
src/storage/types/mapping.rs 148/148 100.00%
src/storage/types/mod.rs 67/91 73.63%
src/storage/types/primitives.rs 564/567 99.47%
src/storage/types/set.rs 454/474 95.78%
src/storage/types/slot.rs 282/296 95.27%
src/storage/types/vec.rs 1078/1095 98.45%
src/test_util.rs 194/231 83.98%
src/tip20/dispatch.rs 584/616 94.81%
src/tip20/mod.rs 1783/1854 96.17%
src/tip20/rewards.rs 444/487 91.17%
src/tip20/roles.rs 187/206 90.78%
src/tip20_factory/dispatch.rs 26/29 89.66%
src/tip20_factory/mod.rs 543/555 97.84%
src/tip403_registry/dispatch.rs 406/443 91.65%
src/tip403_registry/mod.rs 1338/1423 94.03%
src/tip_fee_manager/amm.rs 1111/1147 96.86%
src/tip_fee_manager/dispatch.rs 278/289 96.19%
src/tip_fee_manager/mod.rs 495/510 97.06%
src/validator_config/dispatch.rs 210/221 95.02%
src/validator_config/mod.rs 604/658 91.79%
src/validator_config_v2/dispatch.rs 201/214 93.93%
src/validator_config_v2/mod.rs 1780/1878 94.78%

contracts

Coverage: 209/386 lines (54.15%)

File details
File Lines Coverage
src/lib.rs 1/71 1.41%
src/precompiles/account_keychain.rs 24/30 80.00%
src/precompiles/nonce.rs 9/18 50.00%
src/precompiles/stablecoin_dex.rs 36/48 75.00%
src/precompiles/tip20.rs 52/70 74.29%
src/precompiles/tip20_factory.rs 6/12 50.00%
src/precompiles/tip403_registry.rs 12/15 80.00%
src/precompiles/tip_fee_manager.rs 21/45 46.67%
src/precompiles/validator_config.rs 12/26 46.15%
src/precompiles/validator_config_v2.rs 36/51 70.59%

Total: 20971/22180 lines (94.55%)

📦 Download full HTML report

@howydev howydev marked this pull request as draft February 25, 2026 21:26
…ated

ZELLIC-64: initialize_if_migrated only checked validator count parity
between V1 and V2 but did not verify each validator's activity state.
A validator could be deactivated in V2 during migration while still
active in V1 (or vice versa), leading to inconsistent state.

Add a per-validator check that V1.active matches V2.deactivatedAtHeight
and revert with MigrationStateMismatch(idx) on mismatch.

Amp-Thread-ID: https://ampcode.com/threads/T-019c964b-0561-763e-9d3c-c62a3398ca66
Co-authored-by: Amp <amp@ampcode.com>
@howydev howydev force-pushed the howy/fix-initialize-if-migrated-activity-validation branch from 7672297 to da61a3b Compare February 25, 2026 21:31
@howydev howydev marked this pull request as ready for review February 26, 2026 00:12
Copy link
Contributor

@SuperFluffy SuperFluffy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine, but can be simplified.

Does the spec need to be updated to cover the error?

Add a test to check this activity mismatsch?

Can you double check that this matches the spec?

Comment on lines +623 to +627
if v1_val.active && v2_val.deactivated_at_height != 0 {
Err(ValidatorConfigV2Error::migration_state_mismatch(i))?
}
if !v1_val.active && v2_val.deactivated_at_height == 0 {
Err(ValidatorConfigV2Error::migration_state_mismatch(i))?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's simplify this:

if v1_val.active != (v2_val.deactivated == 0) {
    return Err(ValidatorConfigV2Error::migration_state_mismatch(i));
}


/// @notice Thrown when a migrated validator's active/deactivated state does not match V1
/// @param idx The index of the validator with mismatched state
error MigrationStateMismatch(uint64 idx);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be added to the spec?

for (uint64 i = 0; i < v1Validators.length; i++) {
bool v1Active = v1Validators[i].active;
uint64 v2Deactivated = validatorsArray[i].deactivatedAtHeight;
if (v1Active && v2Deactivated != 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can probably express this in simpler terms similar to the rust code.

let v1_val = v1.validators(v1.validators_array(i)?)?;
let v2_val = self.validators[i as usize].read()?;
if v1_val.active && v2_val.deactivated_at_height != 0 {
Err(ValidatorConfigV2Error::migration_state_mismatch(i))?
Copy link
Contributor

@joshieDo joshieDo Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some smol unit test would be nice

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants