Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions crates/chainspec/src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@
//! Existing hardforks (T0–T2) are mapped to feature sets for backward compatibility.
//! New protocol changes should be defined as features directly, not as hardfork variants.

use alloy_primitives::{Address, U256, address, keccak256};

use crate::hardfork::TempoHardfork;

/// Address of the FeatureRegistry precompile.
pub const FEATURE_REGISTRY_ADDRESS: Address =
address!("0xFEA7000000000000000000000000000000000000");

/// A single feature identified by a numeric ID.
///
/// Features are cheap to copy and compare. The ID is an index into the
Expand Down Expand Up @@ -203,6 +209,56 @@ impl TempoFeatures {
let id = feature.id();
((id / 64) as usize, id % 64)
}

/// Read active features directly from on-chain state via SLOAD.
///
/// Reads the feature bitmap from the `FeatureRegistry` precompile's storage.
/// The bitmap is stored as a `Mapping<u64, U256>` at slot 1 (the second field
/// in the contract struct). Each 256-bit word encodes 256 features.
///
/// Stops reading when a zero word is encountered (assumes features are
/// activated sequentially from word 0).
pub fn from_state(
mut sload: impl FnMut(Address, U256) -> Result<U256, Box<dyn std::error::Error>>,
) -> Result<Self, Box<dyn std::error::Error>> {
let mut features = Self::empty();

// Slot 1 is the base slot for the `features` mapping (Mapping<u64, U256>).
// The actual storage slot for mapping key `k` is keccak256(k || base_slot).
let base_slot = U256::from(1);

for word_index in 0u64..16 {
// Compute Solidity mapping slot: keccak256(abi.encode(key, base_slot))
let key = U256::from(word_index);
let mut buf = [0u8; 64];
buf[0..32].copy_from_slice(&key.to_be_bytes::<32>());
buf[32..64].copy_from_slice(&base_slot.to_be_bytes::<32>());
let slot = U256::from_be_bytes(keccak256(buf).0);

let word = sload(FEATURE_REGISTRY_ADDRESS, slot)?;
if word.is_zero() {
break;
}

// Each U256 word encodes 256 features; convert to our u64-word bitset
let word_bytes = word.to_le_bytes::<32>();
for (sub_idx, chunk) in word_bytes.chunks_exact(8).enumerate() {
let sub_word = u64::from_le_bytes(chunk.try_into().unwrap());
if sub_word == 0 {
continue;
}
for bit in 0..64u32 {
if sub_word & (1u64 << bit) != 0 {
let feature_id =
word_index as u32 * 256 + sub_idx as u32 * 64 + bit;
features.insert(TempoFeature::new(feature_id));
}
}
}
}

Ok(features)
}
}

impl FromIterator<TempoFeature> for TempoFeatures {
Expand Down
91 changes: 91 additions & 0 deletions crates/contracts/src/precompiles/feature_registry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
pub use IFeatureRegistry::IFeatureRegistryErrors as FeatureRegistryError;

crate::sol! {
/// Feature Registry interface for managing protocol feature flags.
///
/// Stores a bitmap of active features and allows the admin to activate,
/// deactivate, or schedule features for timestamp-based activation.
/// The admin can also killswitch (cancel) scheduled features.
#[derive(Debug, PartialEq, Eq)]
#[sol(abi)]
interface IFeatureRegistry {
// =====================================================================
// View functions
// =====================================================================

/// Get a single 256-bit word of the feature bitmap at `index`.
function featureWord(uint64 index) external view returns (uint256);

/// Check whether a feature is currently active.
function isActive(uint32 featureId) external view returns (bool);

/// Get the scheduled activation timestamp for a feature (0 = not scheduled).
function scheduledActivation(uint32 featureId) external view returns (uint64);

/// Get the contract owner / admin.
function owner() external view returns (address);

// =====================================================================
// Mutate functions (owner only)
// =====================================================================

/// Immediately activate a feature.
function activate(uint32 featureId) external;

/// Immediately deactivate a feature (killswitch).
function deactivate(uint32 featureId) external;

/// Schedule a feature to activate at a future timestamp.
function scheduleActivation(uint32 featureId, uint64 activateAt) external;

/// Cancel a scheduled activation (killswitch for scheduled features).
function cancelScheduledActivation(uint32 featureId) external;

/// Transfer admin ownership.
function transferOwnership(address newOwner) external;

// =====================================================================
// Errors
// =====================================================================

error Unauthorized();
error FeatureAlreadyActive(uint32 featureId);
error FeatureNotActive(uint32 featureId);
error FeatureNotScheduled(uint32 featureId);
error FeatureAlreadyScheduled(uint32 featureId);
error InvalidActivationTime();
error InvalidOwner();
}
}

impl FeatureRegistryError {
pub const fn unauthorized() -> Self {
Self::Unauthorized(IFeatureRegistry::Unauthorized {})
}

pub const fn feature_already_active(feature_id: u32) -> Self {
Self::FeatureAlreadyActive(IFeatureRegistry::FeatureAlreadyActive { featureId: feature_id })
}

pub const fn feature_not_active(feature_id: u32) -> Self {
Self::FeatureNotActive(IFeatureRegistry::FeatureNotActive { featureId: feature_id })
}

pub const fn feature_not_scheduled(feature_id: u32) -> Self {
Self::FeatureNotScheduled(IFeatureRegistry::FeatureNotScheduled { featureId: feature_id })
}

pub const fn feature_already_scheduled(feature_id: u32) -> Self {
Self::FeatureAlreadyScheduled(IFeatureRegistry::FeatureAlreadyScheduled {
featureId: feature_id,
})
}

pub const fn invalid_activation_time() -> Self {
Self::InvalidActivationTime(IFeatureRegistry::InvalidActivationTime {})
}

pub const fn invalid_owner() -> Self {
Self::InvalidOwner(IFeatureRegistry::InvalidOwner {})
}
}
4 changes: 4 additions & 0 deletions crates/contracts/src/precompiles/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod account_keychain;
pub mod common_errors;
pub mod feature_registry;
pub mod nonce;
pub mod stablecoin_dex;
pub mod tip20;
Expand All @@ -12,6 +13,7 @@ pub mod validator_config_v2;
pub use account_keychain::*;
use alloy_primitives::{Address, address};
pub use common_errors::*;
pub use feature_registry::*;
pub use nonce::*;
pub use stablecoin_dex::*;
pub use tip_fee_manager::*;
Expand All @@ -35,3 +37,5 @@ pub const ACCOUNT_KEYCHAIN_ADDRESS: Address =
address!("0xAAAAAAAA00000000000000000000000000000000");
pub const VALIDATOR_CONFIG_V2_ADDRESS: Address =
address!("0xCCCCCCCC00000000000000000000000000000001");
pub const FEATURE_REGISTRY_ADDRESS: Address =
address!("0xFEA7000000000000000000000000000000000000");
13 changes: 10 additions & 3 deletions crates/precompiles/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ use revm::{
precompile::{PrecompileError, PrecompileOutput, PrecompileResult},
};
use tempo_contracts::precompiles::{
AccountKeychainError, FeeManagerError, NonceError, RolesAuthError, StablecoinDEXError,
TIP20FactoryError, TIP403RegistryError, TIPFeeAMMError, UnknownFunctionSelector,
ValidatorConfigError, ValidatorConfigV2Error,
AccountKeychainError, FeatureRegistryError, FeeManagerError, NonceError, RolesAuthError,
StablecoinDEXError, TIP20FactoryError, TIP403RegistryError, TIPFeeAMMError,
UnknownFunctionSelector, ValidatorConfigError, ValidatorConfigV2Error,
};

/// Top-level error type for all Tempo precompile operations
Expand Down Expand Up @@ -70,6 +70,10 @@ pub enum TempoPrecompileError {
#[error("Account keychain error: {0:?}")]
AccountKeychainError(AccountKeychainError),

/// Error from feature registry precompile
#[error("Feature registry error: {0:?}")]
FeatureRegistryError(FeatureRegistryError),

#[error("Gas limit exceeded")]
OutOfGas,

Expand Down Expand Up @@ -110,6 +114,7 @@ impl TempoPrecompileError {
| Self::ValidatorConfigError(_)
| Self::ValidatorConfigV2Error(_)
| Self::AccountKeychainError(_)
| Self::FeatureRegistryError(_)
| Self::UnknownFunctionSelector(_) => false,
}
}
Expand Down Expand Up @@ -142,6 +147,7 @@ impl TempoPrecompileError {
Self::ValidatorConfigError(e) => e.abi_encode().into(),
Self::ValidatorConfigV2Error(e) => e.abi_encode().into(),
Self::AccountKeychainError(e) => e.abi_encode().into(),
Self::FeatureRegistryError(e) => e.abi_encode().into(),
Self::OutOfGas => {
return Err(PrecompileError::OutOfGas);
}
Expand Down Expand Up @@ -208,6 +214,7 @@ pub fn error_decoder_registry() -> TempoPrecompileErrorRegistry {
add_errors_to_registry(&mut registry, TempoPrecompileError::ValidatorConfigError);
add_errors_to_registry(&mut registry, TempoPrecompileError::ValidatorConfigV2Error);
add_errors_to_registry(&mut registry, TempoPrecompileError::AccountKeychainError);
add_errors_to_registry(&mut registry, TempoPrecompileError::FeatureRegistryError);

registry
}
Expand Down
144 changes: 144 additions & 0 deletions crates/precompiles/src/feature_registry/dispatch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use super::*;
use crate::{Precompile, dispatch_call, input_cost, mutate_void, view};
use alloy::{primitives::Address, sol_types::SolInterface};
use revm::precompile::{PrecompileError, PrecompileResult};
use tempo_contracts::precompiles::IFeatureRegistry::IFeatureRegistryCalls;

impl Precompile for FeatureRegistry {
fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult {
self.storage
.deduct_gas(input_cost(calldata.len()))
.map_err(|_| PrecompileError::OutOfGas)?;

dispatch_call(
calldata,
IFeatureRegistryCalls::abi_decode,
|call| match call {
IFeatureRegistryCalls::owner(call) => view(call, |_| self.owner()),
IFeatureRegistryCalls::featureWord(call) => {
view(call, |c| self.feature_word(c.index))
}
IFeatureRegistryCalls::isActive(call) => {
view(call, |c| self.is_active(c.featureId))
}
IFeatureRegistryCalls::scheduledActivation(call) => {
view(call, |c| self.scheduled_activation(c.featureId))
}
IFeatureRegistryCalls::activate(call) => {
mutate_void(call, msg_sender, |s, c| self.activate(s, c.featureId))
}
IFeatureRegistryCalls::deactivate(call) => {
mutate_void(call, msg_sender, |s, c| self.deactivate(s, c.featureId))
}
IFeatureRegistryCalls::scheduleActivation(call) => {
mutate_void(call, msg_sender, |s, c| {
self.schedule_activation(s, c.featureId, c.activateAt)
})
}
IFeatureRegistryCalls::cancelScheduledActivation(call) => {
mutate_void(call, msg_sender, |s, c| {
self.cancel_scheduled_activation(s, c.featureId)
})
}
IFeatureRegistryCalls::transferOwnership(call) => {
mutate_void(call, msg_sender, |s, c| {
self.transfer_ownership(s, c.newOwner)
})
}
},
)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{
expect_precompile_revert,
storage::{StorageCtx, hashmap::HashMapStorageProvider},
test_util::{assert_full_coverage, check_selector_coverage},
};
use alloy::sol_types::{SolCall, SolValue};
use tempo_contracts::precompiles::{
FeatureRegistryError, IFeatureRegistry, IFeatureRegistry::IFeatureRegistryCalls,
};

#[test]
fn test_dispatch_owner() -> eyre::Result<()> {
let admin = Address::random();
let mut storage = HashMapStorageProvider::new(1);
StorageCtx::enter(&mut storage, || {
let mut reg = FeatureRegistry::new();
reg.initialize(admin)?;

let calldata = IFeatureRegistry::ownerCall {}.abi_encode();
let result = reg.call(&calldata, admin)?;

assert!(!result.reverted);
let decoded = Address::abi_decode(&result.bytes)?;
assert_eq!(decoded, admin);

Ok(())
})
}

#[test]
fn test_dispatch_activate_and_is_active() -> eyre::Result<()> {
let admin = Address::random();
let mut storage = HashMapStorageProvider::new(1);
StorageCtx::enter(&mut storage, || {
let mut reg = FeatureRegistry::new();
reg.initialize(admin)?;

// Activate feature 5
let calldata = IFeatureRegistry::activateCall { featureId: 5 }.abi_encode();
let result = reg.call(&calldata, admin)?;
assert!(!result.reverted);

// Check isActive
let calldata = IFeatureRegistry::isActiveCall { featureId: 5 }.abi_encode();
let result = reg.call(&calldata, admin)?;
assert!(!result.reverted);
let active = bool::abi_decode(&result.bytes)?;
assert!(active);

Ok(())
})
}

#[test]
fn test_dispatch_unauthorized() -> eyre::Result<()> {
let admin = Address::random();
let non_owner = Address::random();
let mut storage = HashMapStorageProvider::new(1);
StorageCtx::enter(&mut storage, || {
let mut reg = FeatureRegistry::new();
reg.initialize(admin)?;

let calldata = IFeatureRegistry::activateCall { featureId: 0 }.abi_encode();
let result = reg.call(&calldata, non_owner);
expect_precompile_revert(&result, FeatureRegistryError::unauthorized());

Ok(())
})
}

#[test]
fn test_selector_coverage() -> eyre::Result<()> {
let mut storage = HashMapStorageProvider::new(1);
StorageCtx::enter(&mut storage, || {
let mut reg = FeatureRegistry::new();

let unsupported = check_selector_coverage(
&mut reg,
IFeatureRegistryCalls::SELECTORS,
"IFeatureRegistry",
IFeatureRegistryCalls::name_by_selector,
);

assert_full_coverage([unsupported]);

Ok(())
})
}
}
Loading
Loading