Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
6 changes: 3 additions & 3 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ ESPRESSO_DEMO_SEQUENCER_LIBP2P_PORT_4=7004
ESPRESSO_SEQUENCER_STAKE_TABLE_CAPACITY=10

# The demo can use a short exit escrow period. It should be the length of at least 3 epochs plus a
# reasonably timeframe to submit slashing evidence. This assumes the demo is using 20 blocks epoch.
ESPRESSO_SEQUENCER_STAKE_TABLE_EXIT_ESCROW_PERIOD=3m
# reasonably timeframe to submit slashing evidence. The stake table contract requires a minimum of 2 days.
ESPRESSO_SEQUENCER_STAKE_TABLE_EXIT_ESCROW_PERIOD=2d

# Foundry
# The mnemonic used by foundry to deploy contracts.
Expand Down Expand Up @@ -178,4 +178,4 @@ ESPRESSO_OPS_TIMELOCK_EXECUTORS=${ESPRESSO_SEQUENCER_ETH_MULTISIG_ADDRESS}
ESPRESSO_SAFE_EXIT_TIMELOCK_DELAY=1209600
ESPRESSO_SAFE_EXIT_TIMELOCK_ADMIN=8626f6940e2eb28930efb4cef49b2d1f2c9c1199
ESPRESSO_SAFE_EXIT_TIMELOCK_PROPOSERS=${ESPRESSO_SEQUENCER_ETH_MULTISIG_ADDRESS}
ESPRESSO_SAFE_EXIT_TIMELOCK_EXECUTORS=${ESPRESSO_SEQUENCER_ETH_MULTISIG_ADDRESS}
ESPRESSO_SAFE_EXIT_TIMELOCK_EXECUTORS=${ESPRESSO_SEQUENCER_ETH_MULTISIG_ADDRESS}
26 changes: 26 additions & 0 deletions contracts/artifacts/abi/StakeTableV2.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@
],
"stateMutability": "view"
},
{
"type": "function",
"name": "MAX_EXIT_ESCROW_PERIOD",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint64",
"internalType": "uint64"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "MAX_METADATA_URI_LENGTH",
Expand All @@ -43,6 +56,19 @@
],
"stateMutability": "view"
},
{
"type": "function",
"name": "MIN_EXIT_ESCROW_PERIOD",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint64",
"internalType": "uint64"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "PAUSER_ROLE",
Expand Down
8 changes: 4 additions & 4 deletions contracts/rust/adapter/src/bindings/stake_table.rs

Large diffs are not rendered by default.

422 changes: 417 additions & 5 deletions contracts/rust/adapter/src/bindings/stake_table_v2.rs

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion contracts/rust/deployer/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,9 @@ impl<P: Provider + WalletProvider> DeployerArgs<P> {
let lc_addr = contracts
.address(Contract::LightClientProxy)
.context("no LightClient proxy address")?;
let escrow_period = self.exit_escrow_period.unwrap_or(U256::from(250));
let escrow_period = self
.exit_escrow_period
.unwrap_or(U256::from(crate::DEFAULT_EXIT_ESCROW_PERIOD_SECONDS));
crate::deploy_stake_table_proxy(
provider,
contracts,
Expand Down
16 changes: 9 additions & 7 deletions contracts/rust/deployer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ pub fn build_random_provider(url: Url) -> HttpProviderWithWallet {
const LIBRARY_PLACEHOLDER_ADDRESS: &str = "ffffffffffffffffffffffffffffffffffffffff";
/// `stateHistoryRetentionPeriod` in LightClient.sol as the maximum retention period in seconds
pub const MAX_HISTORY_RETENTION_SECONDS: u32 = 864000;
/// Default exit escrow period for stake table (2 days in seconds)
pub const DEFAULT_EXIT_ESCROW_PERIOD_SECONDS: u64 = 172800;

/// Set of predeployed contracts.
#[derive(Clone, Debug, Parser)]
Expand Down Expand Up @@ -1826,7 +1828,7 @@ mod tests {
)
.await?;
let lc_addr = deploy_light_client_contract(&provider, &mut contracts, false).await?;
let exit_escrow_period = U256::from(1000);
let exit_escrow_period = U256::from(DEFAULT_EXIT_ESCROW_PERIOD_SECONDS);

let stake_table_proxy_addr = deploy_stake_table_proxy(
&provider,
Expand Down Expand Up @@ -2430,7 +2432,7 @@ mod tests {
.await?;

// deploy stake table
let exit_escrow_period = U256::from(250);
let exit_escrow_period = U256::from(DEFAULT_EXIT_ESCROW_PERIOD_SECONDS);
let owner = init_recipient;
let stake_table_addr = deploy_stake_table_proxy(
&provider,
Expand Down Expand Up @@ -2489,7 +2491,7 @@ mod tests {
.await?;

// deploy stake table
let exit_escrow_period = U256::from(250);
let exit_escrow_period = U256::from(DEFAULT_EXIT_ESCROW_PERIOD_SECONDS);
let owner = init_recipient;
let stake_table_addr = deploy_stake_table_proxy(
&provider,
Expand Down Expand Up @@ -2600,7 +2602,7 @@ mod tests {
)
.await?;

let exit_escrow_period = U256::from(250);
let exit_escrow_period = U256::from(DEFAULT_EXIT_ESCROW_PERIOD_SECONDS);
let owner = init_recipient;
let stake_table_proxy_addr = deploy_stake_table_proxy(
&provider,
Expand Down Expand Up @@ -3179,7 +3181,7 @@ mod tests {
&mut contracts,
token_addr,
lc_proxy_addr,
U256::from(1000u64),
U256::from(DEFAULT_EXIT_ESCROW_PERIOD_SECONDS),
admin,
)
.await?
Expand Down Expand Up @@ -3421,7 +3423,7 @@ mod tests {
.await?;

let lc_addr = deploy_light_client_contract(&provider, &mut contracts, false).await?;
let exit_escrow_period = U256::from(1000);
let exit_escrow_period = U256::from(DEFAULT_EXIT_ESCROW_PERIOD_SECONDS);

let stake_table_proxy_addr = deploy_stake_table_proxy(
&provider,
Expand Down Expand Up @@ -3458,7 +3460,7 @@ mod tests {
.await?;

let lc_addr = deploy_light_client_contract(&provider, &mut contracts, false).await?;
let exit_escrow_period = U256::from(1000);
let exit_escrow_period = U256::from(DEFAULT_EXIT_ESCROW_PERIOD_SECONDS);

let stake_table_proxy_addr = deploy_stake_table_proxy(
&provider,
Expand Down
3 changes: 1 addition & 2 deletions contracts/src/StakeTable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,7 @@ contract StakeTable is Initializable, InitializedAt, OwnableUpgradeable, UUPSUpg
token = ERC20(_tokenAddress);
lightClient = ILightClient(_lightClientAddress);

uint256 minExitEscrowPeriod = 90 seconds; // assuming 15s per block and min blocks per epoch
// is 6 in the light client
uint256 minExitEscrowPeriod = 2 days;
if (_exitEscrowPeriod < minExitEscrowPeriod) {
revert ExitEscrowPeriodInvalid();
}
Expand Down
34 changes: 26 additions & 8 deletions contracts/src/StakeTableV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,20 @@ contract StakeTableV2 is StakeTable, PausableUpgradeable, AccessControlUpgradeab
/// @notice Maximum commission in basis points (100% = 10000 bps)
uint16 public constant MAX_COMMISSION_BPS = 10000;

/// @notice Minimum exit escrow period (2 days)
/// @dev This is a technical minimum bound enforced by the contract. Setting the exit escrow
/// period
/// to this minimum does not guarantee safety. The actual exit escrow period must be set such
/// that the contract holds funds long enough until they are no longer staked in Espresso,
/// allowing sufficient time for validators to exit the active validator set and for slashing
/// evidence to be submitted. Governance should set a value appropriate for Espresso network
/// parameters (e.g., blocksPerEpoch, blockTime, and epoch duration) to ensure security.
uint64 public constant MIN_EXIT_ESCROW_PERIOD = 2 days;

/// @notice Maximum exit escrow period (14 days)
/// @dev Reasonable upper bound to prevent excessive lockup periods
uint64 public constant MAX_EXIT_ESCROW_PERIOD = 14 days;

/// @notice Minimum time interval between commission increases (in seconds)
uint256 public minCommissionIncreaseInterval;

Expand Down Expand Up @@ -787,21 +801,25 @@ contract StakeTableV2 is StakeTable, PausableUpgradeable, AccessControlUpgradeab
/// @notice Update the exit escrow period
/// @param newExitEscrowPeriod The new exit escrow period
/// @dev This function ensures that the exit escrow period is within the valid range
/// @dev This function is not pausable so that governance can perform emergency updates in the
/// presence of system
/// (MIN_EXIT_ESCROW_PERIOD
/// to MAX_EXIT_ESCROW_PERIOD). However, governance MUST set a value that ensures funds are held
/// until they are no longer staked in Espresso, accounting for validator exit time and slashing
/// evidence submission windows. This function is not pausable so that governance can perform
/// emergency updates in the
/// presence of system upgrades.
function updateExitEscrowPeriod(uint64 newExitEscrowPeriod)
external
virtual
onlyRole(DEFAULT_ADMIN_ROLE)
{
uint64 minExitEscrowPeriod = lightClient.blocksPerEpoch() * 15; // assuming 15 seconds per
// block
uint64 maxExitEscrowPeriod = 86400 * 14; // 14 days

if (newExitEscrowPeriod < minExitEscrowPeriod || newExitEscrowPeriod > maxExitEscrowPeriod)
{
// check if the new exit escrow period is within the valid range
if (
newExitEscrowPeriod < MIN_EXIT_ESCROW_PERIOD
|| newExitEscrowPeriod > MAX_EXIT_ESCROW_PERIOD
) {
revert ExitEscrowPeriodInvalid();
}

exitEscrowPeriod = newExitEscrowPeriod;
emit ExitEscrowPeriodUpdated(newExitEscrowPeriod);
}
Expand Down
6 changes: 3 additions & 3 deletions contracts/test/StakeTable.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1338,8 +1338,8 @@ contract StakeTableUpgradeV2Test is Test {
address defaultAdmin = proxy.owner();
vm.startPrank(defaultAdmin);
vm.expectEmit(false, false, false, true, address(proxy));
emit StakeTableV2.ExitEscrowPeriodUpdated(200 seconds);
proxy.updateExitEscrowPeriod(200 seconds);
emit StakeTableV2.ExitEscrowPeriodUpdated(2 days);
proxy.updateExitEscrowPeriod(2 days);
vm.stopPrank();
}

Expand All @@ -1356,7 +1356,7 @@ contract StakeTableUpgradeV2Test is Test {
IAccessControl.AccessControlUnauthorizedAccount.selector, notAdmin, adminRole
)
);
StakeTableV2(proxy).updateExitEscrowPeriod(200 seconds);
StakeTableV2(proxy).updateExitEscrowPeriod(2 days);
vm.stopPrank();
}

Expand Down
13 changes: 7 additions & 6 deletions contracts/test/StakeTableV2Governance.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { OwnableUpgradeable } from
"@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import { StakeTableUpgradeV2Test } from "./StakeTable.t.sol";
import { StakeTable_register_Test } from "./StakeTable.t.sol";
import { ILightClient } from "../src/interfaces/ILightClient.sol";

/// @title StakeTableV2 Governance Tests
/// @notice Comprehensive tests for governance functions: transferOwnership, grantRole, revokeRole
Expand Down Expand Up @@ -91,12 +92,12 @@ contract StakeTableV2GovernanceTest is Test {
)
);
vm.expectEmit(true, true, true, true, address(proxy));
emit StakeTableV2.ExitEscrowPeriodUpdated(200 seconds);
proxy.updateExitEscrowPeriod(200 seconds);
emit StakeTableV2.ExitEscrowPeriodUpdated(2 days);
proxy.updateExitEscrowPeriod(2 days);
vm.stopPrank();

vm.prank(initialOwner);
proxy.updateExitEscrowPeriod(200 seconds);
proxy.updateExitEscrowPeriod(2 days);
}

function test_TransferOwnership_Success() public {
Expand Down Expand Up @@ -170,7 +171,7 @@ contract StakeTableV2GovernanceTest is Test {
vm.stopPrank();

vm.prank(initialOwner);
proxy.updateExitEscrowPeriod(200 seconds);
proxy.updateExitEscrowPeriod(2 days);
}

function test_TransferOwnership_RevertsWhenNotAdmin() public {
Expand All @@ -196,7 +197,7 @@ contract StakeTableV2GovernanceTest is Test {

vm.startPrank(newOwner);

uint64 newPeriod = 200 seconds;
uint64 newPeriod = 2 days;
proxy.updateExitEscrowPeriod(newPeriod);

proxy.grantRole(proxy.PAUSER_ROLE(), newOwner);
Expand All @@ -221,7 +222,7 @@ contract StakeTableV2GovernanceTest is Test {
IAccessControl.AccessControlUnauthorizedAccount.selector, initialOwner, adminRole
)
);
proxy.updateExitEscrowPeriod(200 seconds);
proxy.updateExitEscrowPeriod(2 days);
vm.stopPrank();
}

Expand Down
4 changes: 2 additions & 2 deletions espresso-dev-node/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use async_trait::async_trait;
use clap::{Parser, ValueEnum};
use espresso_contract_deployer::{
self as deployer, network_config::light_client_genesis_from_stake_table, Contract, Contracts,
DeployedContracts, HttpProviderWithWallet,
DeployedContracts, HttpProviderWithWallet, DEFAULT_EXIT_ESCROW_PERIOD_SECONDS,
};
use espresso_dev_node::{
AltChainInfo, DevInfo, DevNodeVersion, SetHotshotDownReqBody, SetHotshotUpReqBody,
Expand Down Expand Up @@ -500,7 +500,7 @@ async fn main() -> anyhow::Result<()> {
}

// deploy permissionless stake table
let exit_escrow_period = U256::from(250); // 250 sec
let exit_escrow_period = U256::from(DEFAULT_EXIT_ESCROW_PERIOD_SECONDS);
let stake_table_proxy_addr = deployer::deploy_stake_table_proxy(
&provider,
contracts,
Expand Down
9 changes: 6 additions & 3 deletions sequencer/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1227,7 +1227,7 @@ where

#[cfg(any(test, feature = "testing"))]
pub mod test_helpers {
use std::time::Duration;
use std::{cmp::max, time::Duration};

use alloy::{
network::EthereumWallet,
Expand All @@ -1237,7 +1237,7 @@ pub mod test_helpers {
use committable::Committable;
use espresso_contract_deployer::{
builder::DeployerArgsBuilder, network_config::light_client_genesis_from_stake_table,
Contract, Contracts,
Contract, Contracts, DEFAULT_EXIT_ESCROW_PERIOD_SECONDS,
};
use espresso_types::{
v0::traits::{NullEventConsumer, PersistenceOptions, StateCatchup},
Expand Down Expand Up @@ -1470,7 +1470,10 @@ pub mod test_helpers {
.genesis_st_state(genesis_stake)
.blocks_per_epoch(blocks_per_epoch)
.epoch_start_block(epoch_start_block)
.exit_escrow_period(U256::from(blocks_per_epoch * 15 + 100))
.exit_escrow_period(U256::from(max(
blocks_per_epoch * 15 + 100,
DEFAULT_EXIT_ESCROW_PERIOD_SECONDS,
)))
.multisig_pauser(signer.address())
.token_name("Espresso".to_string())
.token_symbol("ESP".to_string())
Expand Down
8 changes: 6 additions & 2 deletions sequencer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,7 @@ pub fn empty_builder_commitment() -> BuilderCommitment {
#[cfg(any(test, feature = "testing"))]
pub mod testing {
use std::{
cmp::max,
collections::{BTreeMap, HashMap},
time::Duration,
};
Expand All @@ -733,7 +734,7 @@ pub mod testing {
use committable::Committable;
use espresso_contract_deployer::{
builder::DeployerArgsBuilder, network_config::light_client_genesis_from_stake_table,
Contract, Contracts,
Contract, Contracts, DEFAULT_EXIT_ESCROW_PERIOD_SECONDS,
};
use espresso_types::{
eth_signature_key::EthKeyPair,
Expand Down Expand Up @@ -995,7 +996,10 @@ pub mod testing {
.genesis_st_state(genesis_stake)
.blocks_per_epoch(blocks_per_epoch)
.epoch_start_block(epoch_start_block)
.exit_escrow_period(U256::from(blocks_per_epoch * 15 + 100))
.exit_escrow_period(U256::from(max(
blocks_per_epoch * 15 + 100,
DEFAULT_EXIT_ESCROW_PERIOD_SECONDS,
)))
.multisig_pauser(self.signer.address())
.token_name("Espresso".to_string())
.token_symbol("ESP".to_string())
Expand Down
9 changes: 6 additions & 3 deletions sequencer/src/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ pub trait ChainConfigPersistence: Sized + Send + Sync {

#[cfg(test)]
mod tests {
use std::{collections::BTreeMap, marker::PhantomData, sync::Arc, time::Duration};
use std::{cmp::max, collections::BTreeMap, marker::PhantomData, sync::Arc, time::Duration};

use alloy::{
network::EthereumWallet,
Expand All @@ -111,7 +111,7 @@ mod tests {
use committable::{Commitment, Committable};
use espresso_contract_deployer::{
builder::DeployerArgsBuilder, network_config::light_client_genesis_from_stake_table,
Contract, Contracts,
Contract, Contracts, DEFAULT_EXIT_ESCROW_PERIOD_SECONDS,
};
use espresso_types::{
traits::{
Expand Down Expand Up @@ -1554,7 +1554,10 @@ mod tests {
.genesis_st_state(genesis_stake)
.blocks_per_epoch(blocks_per_epoch)
.epoch_start_block(1)
.exit_escrow_period(U256::from(blocks_per_epoch * 15 + 100))
.exit_escrow_period(U256::from(max(
blocks_per_epoch * 15 + 100,
DEFAULT_EXIT_ESCROW_PERIOD_SECONDS,
)))
.multisig_pauser(network_config.signer().address())
.token_name("Espresso".to_string())
.token_symbol("ESP".to_string())
Expand Down
Loading