---
# System prepended metadata

title: EigenLayer Integration Upgrade Plan

---

# EigenLayer Integration Upgrade Plan

## Purpose

This document outlines the proposed plan to upgrade Taiyi's existing EigenLayer integration to the Eigenlayer's latest version ELIP-2, which unlocks operator sets, slashing capabilities, and other new features.

The doc aims to provide background knowledge and proposed solution to both internal and external stakeholders for reviews and discussions. 

## Background

### Taiyi Today

> **Note**: This section summarizes the dual AVS design from the [official Luban documentation on AVS Middleware](https://docs.luban.wtf/learn/architecture/on_chain_components/avs_middleware_contracts).


> **Note**: In the Taiyi upgrade, Gateway AVS is renamed to Underwriting AVS to better reflect its role in the preconfirmation system. Thoughout this document, we will refer to the Underwriting AVS as Gateway AVS.

Luban's architecture employs a dual AVS design built on top of EigenLayer. This approach separates responsibilities between two distinct AVS contracts: **ValidatorAVS** and **UnderwritingAVS**. Both inherit from a common abstract base contract called **EigenLayerMiddleware**.

```mermaid

classDiagram
    class EigenLayerMiddleware {
        <<abstract>>
    }
    class TaiyiProposerRegistry {
    }
    class UnderwriterAVS {
    }
    class ValidatorAVS {
    }
    EigenLayerMiddleware <|-- UnderwriterAVS : inherits
    EigenLayerMiddleware <|-- ValidatorAVS : inherits
    TaiyiProposerRegistry --> UnderwriterAVS : manages
    TaiyiProposerRegistry --> ValidatorAVS : manages
    UnderwriterAVS --> ValidatorAVS : coordinates rewards
```



#### 1. Operator Opt-in Process

The system provides a flexible operator registration process that allows operators to participate in one or both AVS roles:

```solidity
// Step 1: Register in EigenLayer as operator
delegationManager.registerAsOperator(...)

// Step 2: Register in the appropriate AVS(s)
// For Validator role only:
validatorAVS.registerOperatorToAVS(operator, operatorSignature)

// For Underwriting role (with or without Validator role):
gatewayAVS.registerOperatorToAVSWithPubKey(operator, operatorSignature, operatorBLSPubKey)
// If also registering for Validator role, add:
validatorAVS.registerOperatorToAVS(operator, operatorSignature)
```

Operators can choose to participate in one or both roles. The BLS public key is required only for operators providing Underwriting services, as it's used to sign preconfirmation commitments. Registering for both roles maximizes potential rewards.

Under the hood, each registration adds the operator to the respective AVS in the EigenLayer AVS Directory and also registers them in the TaiyiProposerRegistry with the appropriate service type.

#### 2. Validator Opt-in Process

Validators can opt into the Luban system through the ValidatorAVS contract:

```solidity
function _registerValidators(
    bytes[] calldata valPubKeys,
    address podOwner,
    bytes calldata delegatedGatewayPubKey
)
```

The process works as follows:

1. **EigenPod Verification**: Validates that the validator has an active EigenPod within EigenLayer.
2. **Delegation to Underwriting**: The validator must delegate to a registered Underwriting operator by providing their BLS public key (`delegatedGatewayPubKey`).
3. **Operator Registration Check**: Verifies that the operator delegated by the pod owner is registered in both Underwriting and Validator AVSs.
4. **Validator Status Check**: Confirms each validator's BLS public key is active within EigenLayer.
5. **Registration Event**: Emits a `ValidatorOperatorRegistered` event, which is captured by off-chain services to handle delegation to relays.

This process enables validators to delegate their blocks to underwriting operators for preconfirmation services.

#### 3. Slashing Mechanism

The slashing mechanism leverages zkVM (SP1) for interactive and non-interactive fraud proofs:

```solidity
contract TaiyiNonInteractiveChallenger is ITaiyiNonInteractiveChallenger, Ownable {
    address public verifierGateway;
    bytes32 public nonInteractiveFraudProofVKey;
    
    // ... 
    
    function proveAType(
        PreconfRequestAType calldata preconfRequestAType,
        bytes calldata signature,
        bytes calldata proofValues,
        bytes calldata proofBytes
    ) external payable {
        // Verify the proof using SP1 zkVM
        ISP1Verifier(verifierGateway).verifyProof(
            nonInteractiveFraudProofVKey, proofValues, proofBytes
        );

    }
}
```

### Latest Changes to EigenLayer 

#### 1. Operator Opt-in

The [ELIP-2](https://github.com/eigenfoundation/ELIPs/blob/main/ELIPs/ELIP-002.md) upgrade introduces a new operator opt-in process via `AllocationManager`.

• **Register as Operator**: Operators register in the EigenLayer protocol by calling `delegationManager.registerAsOperator()`, providing delegation approval settings and metadata.

• **Allocate Stake**: Operators allocate their stake to specific operator sets using `allocationManager.modifyAllocations()`, where stake becomes slashable after a configurable delay period.

• **Register for Sets**: After the allocation delay, operators register for their selected operator sets via `allocationManager.registerForOperatorSets()`, immediately making allocated stake slashable.

• **Update State**: For some AVSs (not applicable to Taiyi), operators may need to update their state in relevant registries using `stakeRegistry.updateOperatorsForQuorum()`.

#### 2. Slashing

The ELIP-2 upgrade introduces a more sophisticated slashing framework that Taiyi will leverage to enhance security guarantees. The new slashing mechanism consists of two main components: the VetoSlasher and InstantSlasher contracts.

The slashing process:

1. The AVS initiates slashing by calling the [slashing contract](https://github.com/Layr-Labs/eigenlayer-middleware/blob/dev/src/slashers/base/SlasherBase.sol) with 'IAllocationManager.SlashingParams'

```solidity
struct SlashingParams {
    address operator;
    uint32 operatorSetId;
    IStrategy[] strategies;
    uint256[] wadsToSlash;
    string description;
}
```

2. For VetoSlasher, the request is queued for the delay period and can be vetoed
3. If not vetoed, or for InstantSlasher, the slashing is executed through `AllocationManager.slashOperator()`
4. For each strategy being slashed:
   - The operator's allocated magnitude to the operator set is reduced
   - The operator's maximum and encumbered magnitudes are decreased proportionally
   - Any pending deallocations are adjusted proportionally
   - The slashed stake is burned through the DelegationManager

## Taiyi after ELIP-2

### Todo No. 1: Update Operator Sets

In the new EigenLayer architecture, the first step in upgrading from dual AVSs to a single AVS is to create dedicated operator sets. Rather than having two separate AVS contracts (ValidatorAVS and UnderwritingAVS), Taiyi will now define two operator sets within a single AVS using the `createOperatorSets` function in the AllocationManager:

```solidity
/**
 * @notice Parameters used by an AVS to create new operator sets
 * @param operatorSetId the id of the operator set to create
 * @param strategies the strategies to add as slashable to the operator set
 */
struct CreateSetParams {
    uint32 operatorSetId;
    IStrategy[] strategies;
}

/**
 * @notice Allows an AVS to create new operator sets, defining strategies that the operator set uses
 */
function createOperatorSets(
    address avs,
    CreateSetParams[] calldata params
)
    external
    checkCanCall(avs)
```

<details>
<summary><b>Example: Creating Validator and Underwriting Operator Sets</b> (click to expand)</summary>

```solidity
// Step 1: Register Taiyi AVS metadata (required before creating operator sets)
allocationManager.updateAVSMetadataURI(
    address(taiyiServiceManager),  // The single AVS contract address 
    "ipfs://QmTaiyiMetadataURI"    // Metadata URI with operator set details
);

// Step 2: Create Validator and Underwriting operator sets
CreateSetParams[] memory setParams = new CreateSetParams[](2);

// Define Validator Operator Set (ID: 1)
setParams[0] = CreateSetParams({
    operatorSetId: 1,  // Validator set ID
    strategies: getValidatorStrategies()  // Array of IStrategy addresses for Validator set
});

// Define Underwriting Operator Set (ID: 2)
setParams[1] = CreateSetParams({
    operatorSetId: 2,  // Underwriting set ID
    strategies: getUnderwritingStrategies()  // Array of IStrategy addresses for Underwriting set
});

// Create both operator sets in a single transaction
allocationManager.createOperatorSets(
    address(taiyiServiceManager),  // AVS address
    setParams
);
```
</details>

Additionally, `SlashingRegistryCoordinator`, which implements [`IAVSRegistrar`](https://github.com/Layr-Labs/eigenlayer-contracts/blob/dev/src/contracts/interfaces/IAVSRegistrar.sol) will be implemented to handle callbacks from the `AllocationManager.registerForOperatorSets()`.

Setting the `SlashingRegistryCoordinator` is done by `AllocationManager.setAvsRegistrar()`.
```solidity
/**
 * @notice Called by an AVS to configure the address that is called when an operator registers
 * or is deregistered from the AVS's operator sets. If not set (or set to 0), defaults
 * to the AVS's address.
 * @param registrar the new registrar address
 */
function setAVSRegistrar(
    address avs,
    IAVSRegistrar registrar
)
    external
    checkCanCall(avs)
```

### Todo No. 2: Validate Opt-in Process

The entry point of Validator registration will stay on the `EigenlayerMiddleware` contract via a dedicated `registerValidator` function, but URC's `Registry` will be called inside of the `registerValidator` function.

One thing worth noting is that `collateralGwei` will need to be set to minimum allowable amount(0.1 ETH) as Taiyi still uses EigenLayer's for colalteral management. This is for two reasons:

1. **Asset Support**: URC only supports native ETH as collateral, but EigenLayer supports diverse assets like stETH, cbETH, and rETH.

2. **Cross-Protocol Double-Slashing Prevention**: By setting `collateralGwei` to minimum amount(0.1 ETH) in URC and handling actual slashing at the EigenLayer level, we avoid the risk of operators being slashed twice for the same violation.

Additionally, the validator registration function's delegation message should be changed to URC's `Delegation` data type

```solidity
import {Delegation} from '@urc/src/ISlasher.sol'


// Old 
function _registerValidators(
    bytes[] calldata valPubKeys,
    address podOwner,
    bytes calldata delegatedGatewayPubKey
)
    internal
    override

// New
function _registerValidators(
    bytes[] calldata valPubKeys,
    address podOwner,
    Delegation calldata delegation
)
    internal
    override
```

### Todo No. 3: Slashing

The following diagram illustrates the architectural relationships between the various components involved in Taiyi's slashing system:

>**Note**: VetoSlasher requires a *submission-first-then-approval/veto-later* interactive process that is out of the flow below. For InstantSlasher, the entry point for slashing request would be URCRegistry, whereas VetoSlasher's entry point would be itself, which is a seperate process that needs to be triggered before the main thread below.

```mermaid
classDiagram
    %% URC Components
    class URCRegistry {
        +slashCommitmentFromOptIn()
    }
    
    %% Bridge Component
    class TaiyiSlasher {
        +slashFromOptIn()
    }
    
    %% EigenLayer Components
    class SlasherBase {
        <<abstract>>
    }
    
    class ISlasher {
        <<interface>>
        +slashFromOptIn()
    }
    
    class InstantSlasher {
        +initiateSlashing()
    }
    
    class VetoSlasher {
        +initiateSlashing()
        +executeSlashing()
        +vetoSlashing()
        +getSlashingStatus()
    }
    
    class IAllocationManager {
        +slashOperator()
    }
    
    %% Inheritance Relationships
    SlasherBase <|-- TaiyiSlasher : inherits
    ISlasher <|.. TaiyiSlasher : implements
    
    %% Functional Relationships
    URCRegistry --> TaiyiSlasher : calls
    TaiyiSlasher --> InstantSlasher : verify proof
    TaiyiSlasher --> VetoSlasher : verify proof
    InstantSlasher --> IAllocationManager : if success, slash operator
    VetoSlasher --> IAllocationManager : if success, slash operator

    
    %% Notes
    class TaiyiSlasher {
        Bridge between URC
        and EigenLayer
    }
    
    class URCRegistry {
        Handles fraud proof
        verification
    }
    
    class InstantSlasher {
        No veto period
    }
    
    class VetoSlasher {
        Includes veto period
    }
    
    class IAllocationManager {
        Executes actual slashing
        on strategies
    }
```

<details>
<summary><b>Example: TaiyiSlasher Contract Implementation</b> (click to expand)</summary>

```solidity
import { SlasherBase } from "@eigenlayer/eigenlayer-middleware/src/slashers/base/SlasherBase.sol";
import { ISlasher } from "@urc/src/interfaces/ISlasher.sol";

contract TaiyiSlasher is SlasherBase, ISlasher {

    struct SlashingConfig {
        uint32 operatorSetId;     // Which operator set to slash
        uint256 slashAmount;      // Amount to slash (in WAD)
        bool isInstantSlashing;   // Whether to use instant slashing
    }
    
    /// @notice Slash an operator for a given commitment
    /// @dev The URC will call this function to slash a registered operator if supplied with a valid commitment and evidence
    /// @param commitment The commitment message
    /// @param evidence Arbitrary evidence for the slashing
    /// @param challenger The address of the challenger
    /// @return slashAmountGwei The amount of Gwei slashed
    function slashFromOptIn(
        Commitment calldata commitment,
        bytes calldata evidence, 
        address challenger
    ) external returns (uint256 slashAmountGwei) {
        // Step 1: Determine which restaking protocol this is targeting
        // Evidence format: abi.encode(RestakingProtocol, violationType, additionalData)
        (uint8 restakingProtocol, bytes32 violationType, bytes memory additionalData) = 
            abi.decode(evidence, (uint8, bytes32, bytes));
            
        // Step 2: Get the slashing configuration for this violation type
        SlashingConfig memory config = slashingConfigs[violationType];
        if (config.operatorSetId == 0) {
            revert("Unknown violation type");
        }
        
        // Step 3: Extract operator address from commitment
        address operator = _extractOperatorFromCommitment(commitment);
        
        // Prepare slashing parameters
        IAllocationManager.SlashingParams memory params = _prepareSlashingParams(
            operator,
            config.operatorSetId,
            config.slashAmount,
            string(abi.encodePacked("URC slash: ", violationType))
        );
        
        // Step 4: Execute slashing through appropriate slasher
        bool slashingExecuted = false;
        
        if (config.isInstantSlashing) {
            // Non-interactive proof (immediate slashing)
            // The instantSlasher returns true if slashing should be executed directly
            bool shouldExecuteDirectly = instantSlasher.initiateSlashing(params);
            
            if (shouldExecuteDirectly) {
                // Call slashOperator directly only if instantSlasher indicates we should
                allocationManager.slashOperator(params);
                slashingExecuted = true;
            }
        } else {
            // Interactive proof (with veto period)
            // Check if a slashing request is already in progress
            uint256 slashingId = vetoSlasher.getSlashingIdForOperator(operator, config.operatorSetId);
            
            if (slashingId != 0) {
                // Existing slashing request found, check its status
                VetoSlasher.SlashingStatus status = vetoSlasher.getSlashingStatus(slashingId);
                
                if (status == VetoSlasher.SlashingStatus.Vetoed) {
                    revert("Slashing request has been vetoed");
                } else if (status == VetoSlasher.SlashingStatus.Pending) {
                    // Check if the veto period has passed
                    uint256 createdAt = vetoSlasher.getSlashingCreatedAt(slashingId);
                    uint256 vetoPeriod = vetoSlasher.getVetoPeriod();
                    
                    if (block.timestamp < createdAt + vetoPeriod) {
                        revert("Veto period has not yet passed");
                    }
                    
                    // Veto period has passed, proceed with the slashing
                    bool shouldExecuteDirectly = vetoSlasher.executeSlashing(slashingId);
                    
                    if (shouldExecuteDirectly) {
                        // Call slashOperator directly only if vetoSlasher indicates we should
                        allocationManager.slashOperator(params);
                        slashingExecuted = true;
                    }
                }
            } else {
                // No existing request, create a new one
                bool shouldExecuteDirectly = vetoSlasher.initiateSlashing(params);
                
                if (shouldExecuteDirectly) {
                    allocationManager.slashOperator(params);
                    slashingExecuted = true;
                }
            }
        }
        
        // Log the slashing outcome
        emit SlashingResult(operator, config.operatorSetId, slashingExecuted);
        
        // Step 5: Return 0 for slashAmountGwei
        // This is because collateral management is handled by EigenLayer
        // The URC only supports ETH as collateral, but EigenLayer supports ERC20 tokens
        return 0;
    }
    
    // Helper function to extract operator address from commitment
    function _extractOperatorFromCommitment(Commitment calldata commitment) 
        internal view returns (address) {
        // Implementation depends on how operator address is encoded in the commitment
        // For example, could be directly included in the payload or derived from it
        // This is a placeholder for the actual implementation
        return address(uint160(uint256(keccak256(commitment.payload))));
    }
    
    // Helper function to prepare slashing parameters
    function _prepareSlashingParams(
        address operator,
        uint32 operatorSetId,
        uint256 slashAmount,
        string memory description
    ) internal view returns (IAllocationManager.SlashingParams memory) {
        // Get operators strategies
        IStrategy[] memory strategies = _getOperatorStrategies(operator, operatorSetId);
        uint256[] memory wadsToSlash = new uint256[](strategies.length);
        
        // Calculate proportional slashing for each strategy
        for (uint256 i = 0; i < strategies.length; i++) {
            wadsToSlash[i] = slashAmount;
        }
        
        return IAllocationManager.SlashingParams({
            operator: operator,
            operatorSetId: operatorSetId,
            strategies: strategies,
            wadsToSlash: wadsToSlash,
            description: description
        });
    }
    
    // Implementation-specific functions would be here
    function _getOperatorStrategies(address operator, uint32 operatorSetId) 
        internal view returns (IStrategy[] memory) {
        // Implementation would get the strategies for a specific operator and set
    }
}
```
</details>



#### Multi-Restaking Protocol Compatibility
The `slashFromOptIn` function is designed to work with multiple restaking protocols, not just EigenLayer. The evidence parameter contains crucial routing information:
                        
```solidity
(uint8 restakingProtocol, bytes32 violationType, bytes memory additionalData) = 
    abi.decode(evidence, (uint8, bytes32, bytes));
```

This design allows:
- Operator-set specific violation handling via the `violationType` parameter
- Additional protocol-specific data via the `additionalData` field



## Open Questions


### Operator Set Creation

In the sample implementation, operator set creation is done on [`SlashingRegistryCoordinator`](https://github.com/Layr-Labs/eigenlayer-middleware/blob/4d63f27247587607beb67f96fdabec4b2c1321ef/src/SlashingRegistryCoordinator.sol#L825). Since Taiyi will not track quorum, what are the issues with calling `IAllocationManager.createOperatorSets()` directly for operator set creation?

### EigenPod vs Regular Node Registration

To participate in Taiyi, validators need to show that
1. they are indeed ETH validators
2. the registering operator indeed owns, or has been authorized to, register validator keys
3. there are slashable collateral

#### For EigenPod owner or its delegated operator, this is established through:

1. Proving a validator is authentic (ETH validator)
In EigenLayer, verification happens through the verifyWithdrawalCredentials function in the EigenPod contract:
```solidity
function verifyWithdrawalCredentials(
    uint64 beaconTimestamp,
    BeaconChainProofs.StateRootProof calldata stateRootProof,
    uint40[] calldata validatorIndices,
    bytes[] calldata validatorFieldsProofs,
    bytes32[][] calldata validatorFields
) external onlyOwnerOrProofSubmitter { ... }
```
Source: [EigenPod.sol Lines 242](https://github.com/Layr-Labs/eigenlayer-contracts/blob/816e66f7da205e9858ffdb4fdf9700024fd45766/src/contracts/pods/EigenPod.sol#L242)
This function proves a validator exists on the beacon chain by:

- Verifying the validator against the beacon state root
- Confirming the validator's credentials via Merkle proof
- Checking the validator is active on the beacon chain

2. Proving ownership/authorization of validator keys
EigenLayer verifies pod ownership through withdrawal credentials verification:
```solidity
// From _verifyWithdrawalCredentials function
require(
    validatorFields.getWithdrawalCredentials() == bytes32(_podWithdrawalCredentials()) || 
    validatorFields.getWithdrawalCredentials() == bytes32(_podCompoundingWithdrawalCredentials()),
    WithdrawalCredentialsNotForEigenPod()
);
```
Source: [EigenPod.sol Lines 438](https://github.com/Layr-Labs/eigenlayer-contracts/blob/816e66f7da205e9858ffdb4fdf9700024fd45766/src/contracts/pods/EigenPod.sol#L438)
This verifies that:
- The validator's withdrawal credentials point to the EigenPod contract
Only the EigenPod owner (or delegated proof submitter) can provide proofs
The withdrawal address configuration proves key ownership

For delegated operators, they could show the delegation via `DelgationManager` contract thereafter. 

3. Confirming collateral backing
EigenLayer ensures validators have collateral through:
```solidity
// The validator must have sufficient stake
uint64 restakedBalanceGwei = validatorFields.getEffectiveBalanceGwei();
```
Source: [EigenPod.sol Line 502](https://github.com/Layr-Labs/eigenlayer-contracts/blob/816e66f7da205e9858ffdb4fdf9700024fd45766/src/contracts/pods/EigenPod.sol#L502)

Collateral is proven by verifying the validator's effective balance in Gwei

#### For an operator who simply runs node infra for liquid staking protocol, this is established through:
1. Currently this is not handled directly by the URC or Taiyi. However, there're no clear incentive for operator to spam registration with fake validator credentials. 
2. URC handles this optmistically via the [`slashRegistration` function](https://github.com/eth-fabric/urc/blob/34bda6ea5029f6cee336dd96c0524e3e35cc6c9f/src/Registry.sol#L215).
3. this is handled during operator registration. During operator registration, `AllocationManager` would ask operator to allocate stake for the target operator set. This implies that slashing a validator in fact is slashing its operator's stake, which could be either native ETH or ERC20 tokens like stETH. 

First I want to confirm that my understanding above is correct. Secondarily, I wanto explore if there are more elegant ways to handle validator registration. 



