feat : Slashing integration in EigenLayer and Datahaven AVS (#345)

## Summary

This PR integrate the slashing feature with EigenLayer. With this PR,
slashing can now be relayed to our Datahaven AVS and then executed
within EigenLayer. In addition some refactoring of the original slashing
pallet has been done.

## Motivation 

To avoid misbehaving actor in the network, Datahaven has implemented a
slashing pallet in which offenses can be reported and then if adequate
can lead to a sanction on the misbehaving node. It incentive nodes to
only follow good behavior in addition to the reward incentive. The
rewards flow is managed directly into EigenLayer (see
https://github.com/datahaven-xyz/datahaven/pull/351).

## Slashing flow


<img width="2355" height="946" alt="Slashing Flow"
src="https://github.com/user-attachments/assets/c1ddc3dc-2a7e-429d-94e0-1e02a3f65246"
/>

## What changes

* Implemented `slashValidatorsOperator` in `DataHavenServiceManager`. It
received all the slashing requests batched (every new era the queued
slashing are being relayed from substrate to Ethereum). It handle the
slashing of the operators reported into the Validator set.
* Added a `slashes_adapter.rs` utility file to remove the duplication
for each runtime. In addition, we made use of the `sol!` macro from
alloy to encode the calldata for the Ethereum call. This avoid rewriting
encoding logic and allow to remove the hardcoded selector value used to
call the slashing function.
* Added some tests in solidity to test the registering and slashing of
an operator in Ethereum via Eigen Layer.
* Added e2e tests that test the injection of a slash request, it being
relayed via the snowbridge relayer and executed by our Datahaven AVS.

## What could be better

* We are only deploying one strategy for now so it is hardcoded in the
slashing flow. We should be able to update the pallet in case we are
adding a new strategy. So communication from Ethereum should be relayed.
* We don't have error being return in case the slashing fail. Which
could happen if we don't have the right number of strategy or the
validator is not registered... etc.
* More tests for the unhappy path
This commit is contained in:
undercover-cactus 2026-01-16 20:49:45 +01:00 committed by GitHub
parent 15e536780d
commit ac28323e7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1265 additions and 728 deletions

View file

@ -1 +1 @@
c23b1c40740f73273dddba8b23b9b1a84003e5a1
5ac2bf9a87f8096ab3d06ae29a9ced446750a884

File diff suppressed because one or more lines are too long

View file

@ -351,6 +351,33 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
_allocationManager.createOperatorSets(address(this), operatorSets);
}
// ============ Slashing Submitter Functions ============
/**
* @notice Slash the operators of the validators set
* @param slashings array of request to slash operator containing the operator to slash, array of proportions to slash and the reason of the slashing.
*/
function slashValidatorsOperator(
SlashingRequest[] calldata slashings
) external onlyRewardsInitiator {
OperatorSet memory operatorSet = OperatorSet({avs: address(this), id: VALIDATORS_SET_ID});
for (uint256 i = 0; i < slashings.length; i++) {
IAllocationManagerTypes.SlashingParams memory slashingParams =
IAllocationManagerTypes.SlashingParams({
operator: slashings[i].operator,
operatorSetId: VALIDATORS_SET_ID,
strategies: slashings[i].strategies,
wadsToSlash: slashings[i].wadsToSlash,
description: slashings[i].description
});
_allocationManager.slashOperator(address(this), slashingParams);
}
emit SlashingComplete();
}
/**
* @notice Internal function to set the rewards initiator
* @param _rewardsInitiator The new rewards initiator address

View file

@ -66,6 +66,9 @@ interface IDataHavenServiceManagerEvents {
/// @param oldInitiator The previous rewards initiator address
/// @param newInitiator The new rewards initiator address
event RewardsInitiatorSet(address indexed oldInitiator, address indexed newInitiator);
/// @notice Emitted when a batch of slashing request is being successfully slashed
event SlashingComplete();
}
/**
@ -77,6 +80,14 @@ interface IDataHavenServiceManager is
IDataHavenServiceManagerErrors,
IDataHavenServiceManagerEvents
{
/// @notice Slashing request sent from the datahaven slashing pallet via snowbridge to slash operators in the validators set in EL.
struct SlashingRequest {
address operator;
IStrategy[] strategies;
uint256[] wadsToSlash;
string description;
}
/// @notice Checks if a validator address is in the allowlist
/// @param validator Address to check
/// @return True if the validator is in the allowlist, false otherwise

View file

@ -0,0 +1,142 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.27;
import {AVSDeployer} from "./utils/AVSDeployer.sol";
import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol";
import {
IAllocationManagerErrors,
IAllocationManager,
IAllocationManagerTypes,
IAllocationManagerEvents
} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
import {DataHavenServiceManager} from "../src/DataHavenServiceManager.sol";
import {
IDataHavenServiceManagerEvents,
IDataHavenServiceManager
} from "../src/interfaces/IDataHavenServiceManager.sol";
import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol";
import "forge-std/Test.sol";
contract SlashingTest is AVSDeployer {
address operator = address(0xabcd);
address public snowbridgeAgent = address(uint160(uint256(keccak256("snowbridgeAgent"))));
function setUp() public virtual {
_deployMockEigenLayerAndAVS();
}
function test_fulfilSlashingRequest() public {
// Allow our operator to register
vm.prank(avsOwner);
serviceManager.addValidatorToAllowlist(operator);
// Configure the rewards initiator (because only the reward agent can submit slashing request)
vm.prank(avsOwner);
serviceManager.setRewardsInitiator(snowbridgeAgent);
vm.prank(operator);
delegationManager.registerAsOperator(address(0), 0, "");
uint32[] memory operatorSetIds = new uint32[](1);
operatorSetIds[0] = serviceManager.VALIDATORS_SET_ID();
IAllocationManagerTypes.RegisterParams memory registerParams =
IAllocationManagerTypes.RegisterParams({
avs: address(serviceManager),
operatorSetIds: operatorSetIds,
data: abi.encodePacked(address(operator))
});
vm.prank(operator);
allocationManager.registerForOperatorSets(operator, registerParams);
DataHavenServiceManager.SlashingRequest[] memory slashings =
new DataHavenServiceManager.SlashingRequest[](1);
uint256[] memory wadsToSlash = new uint256[](3); // 3 wadsToSlash because we have register 3 strategies for the Validator set
wadsToSlash[0] = 1e16;
wadsToSlash[1] = 1e16;
wadsToSlash[2] = 1e16;
OperatorSet memory operatorSet =
OperatorSet({avs: address(serviceManager), id: serviceManager.VALIDATORS_SET_ID()});
IStrategy[] memory strategies = allocationManager.getStrategiesInOperatorSet(operatorSet);
slashings[0] = IDataHavenServiceManager.SlashingRequest(
operator, strategies, wadsToSlash, "Testing slashing"
);
console.log(block.number);
vm.roll(block.number + uint32(7 days) + 1);
console.log(block.number);
// Because the current magnitude for the allocation is 0
uint256[] memory wadsToSlashed = new uint256[](3);
// We emit the event we expect to see.
vm.prank(snowbridgeAgent);
vm.expectEmit();
emit IAllocationManagerEvents.OperatorSlashed(
operator, operatorSet, strategies, wadsToSlashed, "Testing slashing"
);
vm.expectEmit();
emit IDataHavenServiceManagerEvents.SlashingComplete();
serviceManager.slashValidatorsOperator(slashings);
}
function test_fulfilSlashingRequestForOnlyOneStrategy() public {
// Allow our operator to register
vm.prank(avsOwner);
serviceManager.addValidatorToAllowlist(operator);
// Configure the rewards initiator (because only the reward agent can submit slashing request)
vm.prank(avsOwner);
serviceManager.setRewardsInitiator(snowbridgeAgent);
vm.prank(operator);
delegationManager.registerAsOperator(address(0), 0, "");
uint32[] memory operatorSetIds = new uint32[](1);
operatorSetIds[0] = serviceManager.VALIDATORS_SET_ID();
IAllocationManagerTypes.RegisterParams memory registerParams =
IAllocationManagerTypes.RegisterParams({
avs: address(serviceManager),
operatorSetIds: operatorSetIds,
data: abi.encodePacked(address(operator))
});
vm.prank(operator);
allocationManager.registerForOperatorSets(operator, registerParams);
OperatorSet memory operatorSet =
OperatorSet({avs: address(serviceManager), id: serviceManager.VALIDATORS_SET_ID()});
IStrategy[] memory strategies = allocationManager.getStrategiesInOperatorSet(operatorSet);
DataHavenServiceManager.SlashingRequest[] memory slashings =
new DataHavenServiceManager.SlashingRequest[](1);
uint256[] memory wadsToSlash = new uint256[](1); // We only want to slash 1 strategy
wadsToSlash[0] = 1e16;
IStrategy[] memory strategiesToSlash = new IStrategy[](1);
strategiesToSlash[0] = strategies[0];
slashings[0] = IDataHavenServiceManager.SlashingRequest(
operator, strategiesToSlash, wadsToSlash, "Testing slashing"
);
console.log(block.number);
vm.roll(block.number + uint32(7 days) + 1);
console.log(block.number);
// Because the current magnitude for the allocation is 0
uint256[] memory wadsToSlashed = new uint256[](1);
// We emit the event we expect to see.
vm.prank(snowbridgeAgent);
vm.expectEmit();
emit IAllocationManagerEvents.OperatorSlashed(
operator, operatorSet, strategiesToSlash, wadsToSlashed, "Testing slashing"
);
vm.expectEmit();
emit IDataHavenServiceManagerEvents.SlashingComplete();
serviceManager.slashValidatorsOperator(slashings);
}
}

4
operator/Cargo.lock generated
View file

@ -2609,6 +2609,7 @@ dependencies = [
name = "datahaven-mainnet-runtime"
version = "0.13.0"
dependencies = [
"alloy-core",
"bridge-hub-common 0.13.1",
"datahaven-runtime-common",
"dhp-bridge",
@ -2882,6 +2883,7 @@ dependencies = [
"pallet-evm",
"pallet-evm-chain-id",
"pallet-evm-precompile-proxy",
"pallet-external-validator-slashes",
"pallet-external-validators-rewards",
"pallet-migrations",
"pallet-safe-mode",
@ -2905,6 +2907,7 @@ dependencies = [
name = "datahaven-stagenet-runtime"
version = "0.13.0"
dependencies = [
"alloy-core",
"bridge-hub-common 0.13.1",
"datahaven-runtime-common",
"dhp-bridge",
@ -3057,6 +3060,7 @@ dependencies = [
name = "datahaven-testnet-runtime"
version = "0.13.0"
dependencies = [
"alloy-core",
"bridge-hub-common 0.13.1",
"datahaven-runtime-common",
"dhp-bridge",

View file

@ -74,7 +74,7 @@ mod benchmarks {
let era = T::EraIndexProvider::active_era().index;
let dummy = || T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap();
#[extrinsic_call]
_(RawOrigin::Root, era, dummy(), Perbill::from_percent(50), 1);
_(RawOrigin::Root, era, dummy(), Perbill::from_percent(50));
assert_eq!(
Slashes::<T>::get(
@ -102,7 +102,7 @@ mod benchmarks {
#[block]
{
processed = Pallet::<T>::process_slashes_queue(s);
processed = Pallet::<T>::process_slashes_queue(s).unwrap();
}
assert_eq!(UnreportedSlashesQueue::<T>::get().len(), 1);

View file

@ -66,19 +66,15 @@ pub mod weights;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SlashData<AccountId> {
pub validator: AccountId,
pub amount_to_slash: u128,
pub wad_to_slash: u128,
}
/// TODO: populate this with what we need
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SlashDataUtils<AccountId>(pub Vec<SlashData<AccountId>>);
// FIXME (nice to have): Merge with SendMessage trait from pallet external-validator-reward (similar trait)
pub trait SendMessage<AccountId> {
type Message;
type Ticket;
fn build(utils: &SlashDataUtils<AccountId>) -> Option<Self::Message>;
fn build(utils: &Vec<SlashData<AccountId>>, era: u32) -> Option<Self::Message>;
fn validate(message: Self::Message) -> Result<Self::Ticket, SendError>;
@ -101,6 +97,10 @@ pub mod pallet {
},
/// The slashes message was sent correctly.
SlashesMessageSent { message_id: H256 },
/// We injected a slash
SlashInjected { slash_id: T::SlashId, era: u32 },
/// Number of slashes processed
SlashAddedToQueue { number: u32, era: u32 },
}
#[pallet::config]
@ -305,7 +305,6 @@ pub mod pallet {
era: EraIndex,
validator: T::AccountId,
percentage: Perbill,
external_idx: u64,
) -> DispatchResult {
ensure_root(origin)?;
let active_era = T::EraIndexProvider::active_era().index;
@ -325,7 +324,6 @@ pub mod pallet {
era,
validator,
slash_defer_duration,
external_idx,
)
.ok_or(Error::<T>::ErrorComputingSlash)?;
@ -342,6 +340,12 @@ pub mod pallet {
});
NextSlashId::<T>::put(next_slash_id.saturating_add(One::one()));
Self::deposit_event(Event::<T>::SlashInjected {
slash_id: next_slash_id,
era: era_to_consider,
});
Ok(())
}
@ -360,7 +364,12 @@ pub mod pallet {
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
let processed = Self::process_slashes_queue(T::QueuedSlashesProcessedPerBlock::get());
T::WeightInfo::process_slashes_queue(processed)
if let Some(p) = processed {
T::WeightInfo::process_slashes_queue(p)
} else {
T::WeightInfo::process_slashes_queue(0)
}
}
}
}
@ -412,10 +421,10 @@ where
// Fast path for active-era report - most likely.
// `slash_session` cannot be in a future active era. It must be in `active_era` or before.
let (slash_era, external_idx) = if slash_session >= active_era_start_session_index {
// Account for get_external_index read.
let slash_era = if slash_session >= active_era_start_session_index {
add_db_reads_writes(1, 0);
(active_era, T::ExternalIndexProvider::get_external_index())
active_era
} else {
let eras = BondedEras::<T>::get();
add_db_reads_writes(1, 0);
@ -426,7 +435,7 @@ where
.rev()
.find(|&(_, sesh, _)| sesh <= &slash_session)
{
Some((slash_era, _, external_idx)) => (*slash_era, *external_idx),
Some((slash_era, _, _external_idx)) => *slash_era,
// Before bonding period. defensive - should be filtered out.
None => return consumed_weight,
}
@ -468,7 +477,6 @@ where
slash_era,
stash.clone(),
slash_defer_duration,
external_idx,
);
if let Some(mut slash) = slash {
@ -562,13 +570,22 @@ impl<T: Config> Pallet<T> {
fn add_era_slashes_to_queue(active_era: EraIndex) {
let mut slashes: VecDeque<_> = Slashes::<T>::get(active_era).into();
let len = slashes.len();
UnreportedSlashesQueue::<T>::mutate(|queue| queue.append(&mut slashes));
if len > 0 {
Self::deposit_event(Event::<T>::SlashAddedToQueue {
number: len as u32,
era: active_era,
});
}
}
/// Returns number of slashes that were sent to ethereum.
fn process_slashes_queue(amount: u32) -> u32 {
fn process_slashes_queue(amount: u32) -> Option<u32> {
let mut slashes_to_send: Vec<SlashData<T::AccountId>> = vec![];
let _era_index = T::EraIndexProvider::active_era().index;
let era_index = T::EraIndexProvider::active_era().index;
UnreportedSlashesQueue::<T>::mutate(|queue| {
for _ in 0..amount {
@ -579,22 +596,22 @@ impl<T: Config> Pallet<T> {
slashes_to_send.push(SlashData {
validator: slash.validator,
amount_to_slash: 0, // TODO: need to compute how much we slash
wad_to_slash: u128::from_str_radix("10000000000000000", 10).unwrap(), // TODO: need to compute how much we slash (for now it is 1e16)
});
}
});
if slashes_to_send.is_empty() {
return 0;
return None;
}
let slashes_count = slashes_to_send.len() as u32;
let outbound = match T::SendMessage::build(&SlashDataUtils(slashes_to_send)) {
let outbound = match T::SendMessage::build(&slashes_to_send, era_index) {
Some(send_msg) => send_msg,
None => {
log::error!(target: "ext_validators_rewards", "Failed to build outbound message");
return 0;
log::error!(target: "ext_validators_slashes", "Failed to build outbound message");
return None;
}
};
@ -602,28 +619,26 @@ impl<T: Config> Pallet<T> {
let ticket = T::SendMessage::validate(outbound)
.map_err(|e| {
log::error!(
target: "ext_validators_rewards",
target: "ext_validators_slashes",
"Failed to validate outbound message: {:?}",
e
);
return 0;
})
.unwrap();
.ok()?;
let message_id = T::SendMessage::deliver(ticket)
.map_err(|e| {
log::error!(
target: "ext_validators_rewards",
target: "ext_validators_slashes",
"Failed to deliver outbound message: {:?}",
e
);
return 0;
})
.unwrap();
.ok()?;
Self::deposit_event(Event::<T>::SlashesMessageSent { message_id });
slashes_count
Some(slashes_count)
}
}
@ -631,8 +646,6 @@ impl<T: Config> Pallet<T> {
/// rather deferred for several eras.
#[derive(Encode, Decode, RuntimeDebug, TypeInfo, Clone, PartialEq)]
pub struct Slash<AccountId, SlashId> {
/// external index identifying a given set of validators
pub external_idx: u64,
/// The stash ID of the offending validator.
pub validator: AccountId,
/// Reporters of the offence; bounty payout recipients.
@ -648,7 +661,6 @@ impl<AccountId, SlashId: One> Slash<AccountId, SlashId> {
/// Initializes the default object using the given `validator`.
pub fn default_from(validator: AccountId) -> Self {
Self {
external_idx: 0,
validator,
reporters: vec![],
slash_id: One::one(),
@ -670,7 +682,6 @@ pub(crate) fn compute_slash<T: Config>(
slash_era: EraIndex,
stash: T::AccountId,
slash_defer_duration: EraIndex,
external_idx: u64,
) -> Option<Slash<T::AccountId, T::SlashId>> {
let prior_slash_p = ValidatorSlashInEra::<T>::get(slash_era, &stash).unwrap_or(Zero::zero());
@ -691,7 +702,6 @@ pub(crate) fn compute_slash<T: Config>(
let confirmed = slash_defer_duration.is_zero();
Some(Slash {
external_idx,
validator: stash.clone(),
percentage: slash_fraction,
slash_id,

View file

@ -218,7 +218,7 @@ pub struct MockOkOutboundQueue;
impl crate::SendMessage<AccountId> for MockOkOutboundQueue {
type Ticket = ();
type Message = ();
fn build(_: &crate::SlashDataUtils<AccountId>) -> Option<Self::Ticket> {
fn build(_: &Vec<crate::SlashData<AccountId>>, _: u32) -> Option<Self::Ticket> {
Some(())
}
fn validate(_: Self::Ticket) -> Result<Self::Ticket, SendError> {

View file

@ -35,12 +35,10 @@ fn root_can_inject_manual_offence() {
0,
1u64,
Perbill::from_percent(75),
1
));
assert_eq!(
Slashes::<Test>::get(get_slashing_era(0)),
vec![Slash {
external_idx: 1,
validator: 1,
percentage: Perbill::from_percent(75),
confirmed: false,
@ -61,8 +59,7 @@ fn cannot_inject_future_era_offence() {
RuntimeOrigin::root(),
1,
1u64,
Perbill::from_percent(75),
1
Perbill::from_percent(75)
),
Error::<Test>::ProvidedFutureEra
);
@ -79,8 +76,7 @@ fn cannot_inject_era_offence_too_far_in_the_past() {
RuntimeOrigin::root(),
1,
4u64,
Perbill::from_percent(75),
1
Perbill::from_percent(75)
),
Error::<Test>::ProvidedNonSlashableEra
);
@ -95,8 +91,7 @@ fn root_can_cancel_deferred_slash() {
RuntimeOrigin::root(),
0,
1u64,
Perbill::from_percent(75),
1
Perbill::from_percent(75)
));
assert_ok!(ExternalValidatorSlashes::cancel_deferred_slash(
RuntimeOrigin::root(),
@ -116,8 +111,7 @@ fn root_cannot_cancel_deferred_slash_if_outside_deferring_period() {
RuntimeOrigin::root(),
0,
1u64,
Perbill::from_percent(75),
1
Perbill::from_percent(75)
));
start_era(4, 0, 4);
@ -137,8 +131,7 @@ fn root_cannot_cancel_out_of_bounds() {
RuntimeOrigin::root(),
0,
1u64,
Perbill::from_percent(75),
1
Perbill::from_percent(75)
));
assert_noop!(
ExternalValidatorSlashes::cancel_deferred_slash(
@ -159,8 +152,7 @@ fn root_cannot_cancel_duplicates() {
RuntimeOrigin::root(),
0,
1u64,
Perbill::from_percent(75),
1
Perbill::from_percent(75)
));
assert_noop!(
ExternalValidatorSlashes::cancel_deferred_slash(RuntimeOrigin::root(), 3, vec![0, 0]),
@ -177,15 +169,13 @@ fn root_cannot_cancel_if_not_sorted() {
RuntimeOrigin::root(),
0,
1u64,
Perbill::from_percent(75),
1
Perbill::from_percent(75)
));
assert_ok!(ExternalValidatorSlashes::force_inject_slash(
RuntimeOrigin::root(),
0,
2u64,
Perbill::from_percent(75),
2
Perbill::from_percent(75)
));
assert_noop!(
ExternalValidatorSlashes::cancel_deferred_slash(RuntimeOrigin::root(), 3, vec![1, 0]),
@ -206,14 +196,12 @@ fn test_after_bonding_period_we_can_remove_slashes() {
RuntimeOrigin::root(),
0,
1u64,
Perbill::from_percent(75),
1
Perbill::from_percent(75)
));
assert_eq!(
Slashes::<Test>::get(get_slashing_era(0)),
vec![Slash {
external_idx: 1,
validator: 1,
percentage: Perbill::from_percent(75),
confirmed: false,
@ -250,7 +238,6 @@ fn test_on_offence_injects_offences() {
assert_eq!(
Slashes::<Test>::get(get_slashing_era(0)),
vec![Slash {
external_idx: 0,
validator: 3,
percentage: Perbill::from_percent(75),
confirmed: false,
@ -316,13 +303,11 @@ fn defer_period_of_zero_confirms_immediately_slashes() {
RuntimeOrigin::root(),
0,
1u64,
Perbill::from_percent(75),
1
Perbill::from_percent(75)
));
assert_eq!(
Slashes::<Test>::get(get_slashing_era(0)),
vec![Slash {
external_idx: 1,
validator: 1,
percentage: Perbill::from_percent(75),
confirmed: true,
@ -342,8 +327,7 @@ fn we_cannot_cancel_anything_with_defer_period_zero() {
RuntimeOrigin::root(),
0,
1u64,
Perbill::from_percent(75),
1
Perbill::from_percent(75)
));
assert_noop!(
ExternalValidatorSlashes::cancel_deferred_slash(RuntimeOrigin::root(), 0, vec![0]),
@ -371,7 +355,6 @@ fn test_on_offence_defer_period_0() {
assert_eq!(
Slashes::<Test>::get(get_slashing_era(1)),
vec![Slash {
external_idx: 0,
validator: 3,
percentage: Perbill::from_percent(75),
confirmed: true,
@ -404,7 +387,6 @@ fn test_slashes_command_matches_event() {
assert_eq!(
Slashes::<Test>::get(get_slashing_era(1)),
vec![Slash {
external_idx: 0,
validator: 3,
percentage: Perbill::from_percent(75),
confirmed: true,
@ -463,7 +445,6 @@ fn test_account_id_encoding() {
let alice_account: [u8; 32] = [4u8; 32];
let slash = Slash::<AccountId20, u32> {
external_idx: 0,
validator: AccountId20::from(alice_account),
reporters: vec![],
slash_id: 1,

View file

@ -16,6 +16,7 @@ pallet-authorship = { workspace = true }
pallet-balances = { workspace = true }
pallet-external-validators-rewards = { workspace = true }
pallet-timestamp = { workspace = true }
pallet-external-validator-slashes = { workspace = true }
pallet-evm = { workspace = true }
pallet-evm-chain-id = { workspace = true }
pallet-evm-precompile-proxy = { workspace = true }

View file

@ -28,6 +28,7 @@ pub use migrations::*;
pub mod rewards_adapter;
pub mod safe_mode;
pub use safe_mode::*;
pub mod slashes_adapter;
use fp_account::EthereumSignature;
pub use sp_runtime::OpaqueExtrinsic as UncheckedExtrinsic;

View file

@ -0,0 +1,125 @@
use alloy_core::{
primitives::{Address, U256},
sol,
sol_types::SolCall,
};
use pallet_external_validator_slashes::SlashData;
use snowbridge_outbound_queue_primitives::v2::SendMessage;
use snowbridge_outbound_queue_primitives::v2::{Command, Message as OutboundMessage};
use snowbridge_outbound_queue_primitives::SendError;
use sp_core::{H160, H256};
use sp_std::vec;
use sp_std::vec::Vec;
use crate::AccountId;
sol! {
// Slashing request to be send to the DatahavenServiceManager
struct SlashingRequest {
address operator;
address[] strategies;
uint256[] wadsToSlash;
string description;
}
// function to call in the DatahavenServiceManager to process all the slashing requests (batching)
function slashValidatorsOperator(SlashingRequest[] calldata slashings) external;
}
/// Gas limit for the submitRewards call on Ethereum.
pub const SLASH_VALIDATORS_GAS_LIMIT: u64 = 1_000_000;
/// Configuration for slashes submission.
///
/// Runtimes implement this trait to provide environment-specific values
/// such as contract address and the slash agent origin.
pub trait SlashesSubmissionConfig {
type OutboundQueue: snowbridge_outbound_queue_primitives::v2::SendMessage<
Ticket = OutboundMessage,
>;
/// Get the DataHaven ServiceManager contract address on Ethereum.
fn service_manager_address() -> H160;
/// Get the agent origin for outbound messages.
fn slashes_agent_origin() -> H256;
/// Get the strategies to slash.
fn strategies() -> Vec<Address>;
}
/// Generic slashes submission adapter.
///
/// This adapter implements [`SendMessage`] and uses the configuration provided
/// by [`SlashesSubmissionConfig`] to build, validate, and deliver slashes
/// messages to EigenLayer via Snowbridge.
pub struct SlashesSubmissionAdapter<C>(core::marker::PhantomData<C>);
impl<C: SlashesSubmissionConfig> pallet_external_validator_slashes::SendMessage<AccountId>
for SlashesSubmissionAdapter<C>
{
type Message = OutboundMessage;
type Ticket = OutboundMessage;
fn build(slashes_utils: &Vec<SlashData<AccountId>>, era: u32) -> Option<Self::Message> {
let strategies = C::strategies();
let calldata = encode_slashing_request(slashes_utils, strategies);
let command = Command::CallContract {
target: C::service_manager_address(),
calldata,
gas: SLASH_VALIDATORS_GAS_LIMIT,
value: 0,
};
let message = OutboundMessage {
origin: C::slashes_agent_origin(),
id: H256::from_low_u64_be(era as u64).into(),
fee: 0,
commands: match vec![command].try_into() {
Ok(cmds) => cmds,
Err(_) => {
log::error!(
target: "slashes_send_adapter",
"Failed to convert commands: too many commands"
);
return None;
}
},
};
Some(message)
}
fn validate(message: Self::Message) -> Result<Self::Ticket, SendError> {
C::OutboundQueue::validate(&message)
}
fn deliver(message: Self::Ticket) -> Result<H256, SendError> {
C::OutboundQueue::deliver(message)
}
}
fn encode_slashing_request(
slashes_utils: &Vec<SlashData<AccountId>>,
strategies: Vec<Address>,
) -> Vec<u8> {
let mut slashings: Vec<SlashingRequest> = vec![];
let strategies_len = strategies.len();
// Extend with operator address to slash
for slash_operator in slashes_utils {
// slashing all the strategies
let wads_to_slash = vec![U256::from(slash_operator.wad_to_slash); strategies_len];
let slashing_request = SlashingRequest {
operator: Address::from(slash_operator.validator.0),
strategies: strategies.clone(),
wadsToSlash: wads_to_slash, // We only have one strategy deployed
description: "Slashing validator".into(),
};
slashings.push(slashing_request);
}
// Use the `slashValidatorsOperator` function defined in the sol! macro to build the Ethereum call and encoded it correctly
let calldata = slashValidatorsOperatorCall { slashings }.abi_encode();
return calldata;
}

View file

@ -13,6 +13,7 @@ version = { workspace = true }
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
alloy-core = { workspace = true, features = ["sol-types"] }
bridge-hub-common = { workspace = true, optional = true }
codec = { workspace = true, features = ["derive"] }
datahaven-runtime-common = { workspace = true }

View file

@ -30,6 +30,7 @@ use super::{
Signature, System, Timestamp, Treasury, TxPause, BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT,
MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT, NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION,
};
use alloy_core::primitives::Address;
use codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_runtime::{traits::AccountIdConversion, RuntimeDebug};
@ -103,7 +104,7 @@ use frame_support::{
weights::{constants::RocksDbWeight, IdentityFee, RuntimeDbWeight, Weight},
PalletId,
};
use frame_system::{limits::BlockLength, unique, EnsureRoot, EnsureRootWithSuccess};
use frame_system::{limits::BlockLength, EnsureRoot, EnsureRootWithSuccess};
use governance::councils::*;
use pallet_ethereum::PostLogContent;
use pallet_evm::{
@ -123,7 +124,7 @@ use snowbridge_core::{gwei, meth, AgentIdOf, PricingParameters, Rewards, TokenId
use snowbridge_inbound_queue_primitives::RewardLedger;
use snowbridge_outbound_queue_primitives::{
v1::{Fee, Message, SendMessage},
v2::{Command, ConstantGasMeter, Message as OutboundMessage, SendMessage as SendMessageV2},
v2::{Command, ConstantGasMeter},
SendError, SendMessageFeeProvider,
};
use snowbridge_pallet_outbound_queue_v2::OnNewCommitment;
@ -1678,51 +1679,39 @@ impl pallet_tx_pause::Config for Runtime {
type WeightInfo = mainnet_weights::pallet_tx_pause::WeightInfo<Runtime>;
}
// Stub SendMessage implementation for slash pallet
pub struct SlashesSendAdapter;
impl pallet_external_validator_slashes::SendMessage<AccountId> for SlashesSendAdapter {
type Message = OutboundMessage;
type Ticket = OutboundMessage;
fn build(
_slashes_utils: &pallet_external_validator_slashes::SlashDataUtils<AccountId>,
) -> Option<Self::Message> {
let calldata = Vec::new();
/// Mainnet slashes configuration for EigenLayer submission.
pub struct MainnetSlashesConfig;
let command = Command::CallContract {
target:
runtime_params::dynamic_params::runtime_config::DatahavenServiceManagerAddress::get(
),
calldata,
gas: 1_000_000, // TODO: Determine appropriate gas value after testing
value: 0,
};
let message = OutboundMessage {
origin: runtime_params::dynamic_params::runtime_config::RewardsAgentOrigin::get(), // TODO: get the slash agent address
// TODO: Determine appropriate id value
id: unique(1).into(),
fee: 0,
commands: match vec![command].try_into() {
Ok(cmds) => cmds,
Err(_) => {
log::error!(
target: "slashes_send_adapter",
"Failed to convert commands: too many commands"
);
return None;
}
},
};
Some(message)
impl datahaven_runtime_common::slashes_adapter::SlashesSubmissionConfig for MainnetSlashesConfig {
type OutboundQueue = EthereumOutboundQueueV2;
fn service_manager_address() -> H160 {
runtime_params::dynamic_params::runtime_config::DatahavenServiceManagerAddress::get()
}
fn validate(message: Self::Message) -> Result<Self::Ticket, SendError> {
EthereumOutboundQueueV2::validate(&message)
fn slashes_agent_origin() -> H256 {
runtime_params::dynamic_params::runtime_config::RewardsAgentOrigin::get()
// TODO: Can we use the same as reward and just rename the config to `AgentOrigin` ?
}
fn deliver(message: Self::Ticket) -> Result<H256, SendError> {
EthereumOutboundQueueV2::deliver(message)
fn strategies() -> Vec<Address> {
// We only slash strategy that we reward
let mut strategies: Vec<Address> =
runtime_params::dynamic_params::runtime_config::RewardsStrategiesAndMultipliers::get()
.iter()
.map(|(strategy, _mult)| Address::from(strategy.as_fixed_bytes()))
.collect();
// The array of strategies need to be in ascending order (see https://github.com/Layr-Labs/eigenlayer-contracts/blob/7ecc83c7b180850531bc5b8b953a7340adeecd43/src/contracts/core/AllocationManager.sol#L343-L347)
strategies.sort();
return strategies;
}
}
// Stub SendMessage implementation for slash pallet
pub type SlashesSendAdapter =
datahaven_runtime_common::slashes_adapter::SlashesSubmissionAdapter<MainnetSlashesConfig>;
impl pallet_external_validator_slashes::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type ValidatorId = AccountId;

View file

@ -324,9 +324,8 @@ pub mod dynamic_params {
#[codec(index = 35)]
#[allow(non_upper_case_globals)]
/// The Selector is the first 4 bytes of the keccak256 hash of the function signature("slashValidatorsOperator(address[])")
pub static SlashOperatorSelector: BoundedVec<u8, ConstU32<4>> =
BoundedVec::truncate_from(vec![0xca, 0x48, 0x11, 0x9f]);
/// Temporary placeholder.
pub static Placeholder: H160 = H160::repeat_byte(0x0);
#[codec(index = 36)]
#[allow(non_upper_case_globals)]

View file

@ -13,6 +13,7 @@ version = { workspace = true }
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
alloy-core = { workspace = true, features = ["sol-types"] }
bridge-hub-common = { workspace = true, optional = true }
codec = { workspace = true, features = ["derive"] }
datahaven-runtime-common = { workspace = true }
@ -183,6 +184,7 @@ snowbridge-pallet-system-v2 = { workspace = true }
[features]
default = ["std"]
std = [
"alloy-core/std",
"codec/std",
"datahaven-runtime-common/std",
"fp-account/std",

View file

@ -30,6 +30,7 @@ use super::{
Signature, System, Timestamp, Treasury, TxPause, BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT,
MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT, NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION,
};
use alloy_core::primitives::Address;
use codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_runtime::{traits::AccountIdConversion, RuntimeDebug};
@ -103,7 +104,7 @@ use frame_support::{
weights::{constants::RocksDbWeight, IdentityFee, RuntimeDbWeight, Weight},
PalletId,
};
use frame_system::{limits::BlockLength, unique, EnsureRoot, EnsureRootWithSuccess};
use frame_system::{limits::BlockLength, EnsureRoot, EnsureRootWithSuccess};
use governance::councils::*;
use pallet_ethereum::PostLogContent;
use pallet_evm::{
@ -123,7 +124,7 @@ use snowbridge_core::{gwei, meth, AgentIdOf, PricingParameters, Rewards, TokenId
use snowbridge_inbound_queue_primitives::RewardLedger;
use snowbridge_outbound_queue_primitives::{
v1::{Fee, Message, SendMessage},
v2::{Command, ConstantGasMeter, Message as OutboundMessage, SendMessage as SendMessageV2},
v2::{Command, ConstantGasMeter},
SendError, SendMessageFeeProvider,
};
use snowbridge_pallet_outbound_queue_v2::OnNewCommitment;
@ -1674,51 +1675,39 @@ impl pallet_tx_pause::Config for Runtime {
type WeightInfo = stagenet_weights::pallet_tx_pause::WeightInfo<Runtime>;
}
// Stub SendMessage implementation for slash pallet
pub struct SlashesSendAdapter;
impl pallet_external_validator_slashes::SendMessage<AccountId> for SlashesSendAdapter {
type Message = OutboundMessage;
type Ticket = OutboundMessage;
fn build(
_slashes_utils: &pallet_external_validator_slashes::SlashDataUtils<AccountId>,
) -> Option<Self::Message> {
let calldata = Vec::new();
/// Stagenet slashes configuration for EigenLayer submission.
pub struct StagenetSlashesConfig;
let command = Command::CallContract {
target:
runtime_params::dynamic_params::runtime_config::DatahavenServiceManagerAddress::get(
),
calldata,
gas: 1_000_000, // TODO: Determine appropriate gas value after testing
value: 0,
};
let message = OutboundMessage {
origin: runtime_params::dynamic_params::runtime_config::RewardsAgentOrigin::get(), // TODO: get the slash agent address
// TODO: Determine appropriate id value
id: unique(1).into(),
fee: 0,
commands: match vec![command].try_into() {
Ok(cmds) => cmds,
Err(_) => {
log::error!(
target: "slashes_send_adapter",
"Failed to convert commands: too many commands"
);
return None;
}
},
};
Some(message)
impl datahaven_runtime_common::slashes_adapter::SlashesSubmissionConfig for StagenetSlashesConfig {
type OutboundQueue = EthereumOutboundQueueV2;
fn service_manager_address() -> H160 {
runtime_params::dynamic_params::runtime_config::DatahavenServiceManagerAddress::get()
}
fn validate(message: Self::Message) -> Result<Self::Ticket, SendError> {
EthereumOutboundQueueV2::validate(&message)
fn slashes_agent_origin() -> H256 {
runtime_params::dynamic_params::runtime_config::RewardsAgentOrigin::get()
// TODO: Can we use the same as reward and just rename the config to `AgentOrigin` ?
}
fn deliver(message: Self::Ticket) -> Result<H256, SendError> {
EthereumOutboundQueueV2::deliver(message)
fn strategies() -> Vec<Address> {
// We only slash strategy that we reward
let mut strategies: Vec<Address> =
runtime_params::dynamic_params::runtime_config::RewardsStrategiesAndMultipliers::get()
.iter()
.map(|(strategy, _mult)| Address::from(strategy.as_fixed_bytes()))
.collect();
// The array of strategies need to be in ascending order (see https://github.com/Layr-Labs/eigenlayer-contracts/blob/7ecc83c7b180850531bc5b8b953a7340adeecd43/src/contracts/core/AllocationManager.sol#L343-L347)
strategies.sort();
return strategies;
}
}
// Stub SendMessage implementation for slash pallet
pub type SlashesSendAdapter =
datahaven_runtime_common::slashes_adapter::SlashesSubmissionAdapter<StagenetSlashesConfig>;
impl pallet_external_validator_slashes::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type ValidatorId = AccountId;

View file

@ -327,9 +327,8 @@ pub mod dynamic_params {
#[codec(index = 35)]
#[allow(non_upper_case_globals)]
/// The Selector is the first 4 bytes of the keccak256 hash of the function signature("slashValidatorsOperator(address[])")
pub static SlashOperatorSelector: BoundedVec<u8, ConstU32<4>> =
BoundedVec::truncate_from(vec![0xca, 0x48, 0x11, 0x9f]);
/// Temporary placeholder.
pub static Placeholder: H160 = H160::repeat_byte(0x0);
#[codec(index = 36)]
#[allow(non_upper_case_globals)]

View file

@ -13,6 +13,7 @@ version = { workspace = true }
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
alloy-core = { workspace = true, features = ["sol-types"] }
bridge-hub-common = { workspace = true, optional = true }
codec = { workspace = true, features = ["derive"] }
datahaven-runtime-common = { workspace = true }
@ -183,6 +184,7 @@ snowbridge-pallet-system-v2 = { workspace = true }
[features]
default = ["std"]
std = [
"alloy-core/std",
"codec/std",
"datahaven-runtime-common/std",
"fp-account/std",

View file

@ -30,6 +30,7 @@ use super::{
Signature, System, Timestamp, Treasury, TxPause, BLOCK_HASH_COUNT, EXTRINSIC_BASE_WEIGHT,
MAXIMUM_BLOCK_WEIGHT, NORMAL_BLOCK_WEIGHT, NORMAL_DISPATCH_RATIO, SLOT_DURATION, VERSION,
};
use alloy_core::primitives::Address;
use codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_runtime::{traits::AccountIdConversion, RuntimeDebug};
@ -103,7 +104,7 @@ use frame_support::{
weights::{constants::RocksDbWeight, IdentityFee, RuntimeDbWeight, Weight},
PalletId,
};
use frame_system::{limits::BlockLength, unique, EnsureRoot, EnsureRootWithSuccess};
use frame_system::{limits::BlockLength, EnsureRoot, EnsureRootWithSuccess};
use governance::councils::*;
use pallet_ethereum::PostLogContent;
use pallet_evm::{
@ -123,7 +124,7 @@ use snowbridge_core::{gwei, meth, AgentIdOf, PricingParameters, Rewards, TokenId
use snowbridge_inbound_queue_primitives::RewardLedger;
use snowbridge_outbound_queue_primitives::{
v1::{Fee, Message, SendMessage},
v2::{Command, ConstantGasMeter, Message as OutboundMessage, SendMessage as SendMessageV2},
v2::{Command, ConstantGasMeter},
SendError, SendMessageFeeProvider,
};
use snowbridge_pallet_outbound_queue_v2::OnNewCommitment;
@ -1678,51 +1679,38 @@ impl pallet_tx_pause::Config for Runtime {
type WeightInfo = testnet_weights::pallet_tx_pause::WeightInfo<Runtime>;
}
// Stub SendMessage implementation for slash pallet
pub struct SlashesSendAdapter;
impl pallet_external_validator_slashes::SendMessage<AccountId> for SlashesSendAdapter {
type Message = OutboundMessage;
type Ticket = OutboundMessage;
fn build(
_slashes_utils: &pallet_external_validator_slashes::SlashDataUtils<AccountId>,
) -> Option<Self::Message> {
let calldata = Vec::new();
/// Testnet slashes configuration for EigenLayer submission.
pub struct TestnetSlashesConfig;
let command = Command::CallContract {
target:
runtime_params::dynamic_params::runtime_config::DatahavenServiceManagerAddress::get(
),
calldata,
gas: 1_000_000, // TODO: Determine appropriate gas value after testing
value: 0,
};
let message = OutboundMessage {
origin: runtime_params::dynamic_params::runtime_config::RewardsAgentOrigin::get(), // TODO: get the slash agent address
// TODO: Determine appropriate id value
id: unique(1).into(),
fee: 0,
commands: match vec![command].try_into() {
Ok(cmds) => cmds,
Err(_) => {
log::error!(
target: "slashes_send_adapter",
"Failed to convert commands: too many commands"
);
return None;
}
},
};
Some(message)
impl datahaven_runtime_common::slashes_adapter::SlashesSubmissionConfig for TestnetSlashesConfig {
type OutboundQueue = EthereumOutboundQueueV2;
fn service_manager_address() -> H160 {
runtime_params::dynamic_params::runtime_config::DatahavenServiceManagerAddress::get()
}
fn validate(message: Self::Message) -> Result<Self::Ticket, SendError> {
EthereumOutboundQueueV2::validate(&message)
fn slashes_agent_origin() -> H256 {
runtime_params::dynamic_params::runtime_config::RewardsAgentOrigin::get()
// TODO: Can we use the same as reward and just rename the config to `AgentOrigin` ?
}
fn deliver(message: Self::Ticket) -> Result<H256, SendError> {
EthereumOutboundQueueV2::deliver(message)
fn strategies() -> Vec<Address> {
// We only slash strategy that we reward
let mut strategies: Vec<Address> =
runtime_params::dynamic_params::runtime_config::RewardsStrategiesAndMultipliers::get()
.iter()
.map(|(strategy, _mult)| Address::from(strategy.as_fixed_bytes()))
.collect();
// The array of strategies need to be in ascending order (see https://github.com/Layr-Labs/eigenlayer-contracts/blob/7ecc83c7b180850531bc5b8b953a7340adeecd43/src/contracts/core/AllocationManager.sol#L343-L347)
strategies.sort();
return strategies;
}
}
// Stub SendMessage implementation for slash pallet
pub type SlashesSendAdapter =
datahaven_runtime_common::slashes_adapter::SlashesSubmissionAdapter<TestnetSlashesConfig>;
impl pallet_external_validator_slashes::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type ValidatorId = AccountId;

View file

@ -325,9 +325,8 @@ pub mod dynamic_params {
#[codec(index = 35)]
#[allow(non_upper_case_globals)]
/// The Selector is the first 4 bytes of the keccak256 hash of the function signature("slashValidatorsOperator(address[])")
pub static SlashOperatorSelector: BoundedVec<u8, ConstU32<4>> =
BoundedVec::truncate_from(vec![0xca, 0x48, 0x11, 0x9f]);
/// Temporary placeholder.
pub static Placeholder: H160 = H160::repeat_byte(0x0);
#[codec(index = 36)]
#[allow(non_upper_case_globals)]

View file

@ -1,5 +1,5 @@
{
"version": "0.1.0-autogenerated.11986612280028596972",
"version": "0.1.0-autogenerated.12034382122763786771",
"name": "@polkadot-api/descriptors",
"files": [
"dist"

Binary file not shown.

View file

@ -2207,6 +2207,29 @@ export const dataHavenServiceManagerAbi = [
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'function',
inputs: [
{
name: 'slashings',
internalType: 'struct IDataHavenServiceManager.SlashingRequest[]',
type: 'tuple[]',
components: [
{ name: 'operator', internalType: 'address', type: 'address' },
{
name: 'strategies',
internalType: 'contract IStrategy[]',
type: 'address[]',
},
{ name: 'wadsToSlash', internalType: 'uint256[]', type: 'uint256[]' },
{ name: 'description', internalType: 'string', type: 'string' },
],
},
],
name: 'slashValidatorsOperator',
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'function',
inputs: [],
@ -2413,6 +2436,7 @@ export const dataHavenServiceManagerAbi = [
],
name: 'RewardsSubmitted',
},
{ type: 'event', anonymous: false, inputs: [], name: 'SlashingComplete' },
{
type: 'event',
anonymous: false,
@ -10793,6 +10817,15 @@ export const writeDataHavenServiceManagerSetSnowbridgeGateway =
functionName: 'setSnowbridgeGateway',
})
/**
* Wraps __{@link writeContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"slashValidatorsOperator"`
*/
export const writeDataHavenServiceManagerSlashValidatorsOperator =
/*#__PURE__*/ createWriteContract({
abi: dataHavenServiceManagerAbi,
functionName: 'slashValidatorsOperator',
})
/**
* Wraps __{@link writeContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"submitRewards"`
*/
@ -10943,6 +10976,15 @@ export const simulateDataHavenServiceManagerSetSnowbridgeGateway =
functionName: 'setSnowbridgeGateway',
})
/**
* Wraps __{@link simulateContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"slashValidatorsOperator"`
*/
export const simulateDataHavenServiceManagerSlashValidatorsOperator =
/*#__PURE__*/ createSimulateContract({
abi: dataHavenServiceManagerAbi,
functionName: 'slashValidatorsOperator',
})
/**
* Wraps __{@link simulateContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"submitRewards"`
*/
@ -11039,6 +11081,15 @@ export const watchDataHavenServiceManagerRewardsSubmittedEvent =
eventName: 'RewardsSubmitted',
})
/**
* Wraps __{@link watchContractEvent}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `eventName` set to `"SlashingComplete"`
*/
export const watchDataHavenServiceManagerSlashingCompleteEvent =
/*#__PURE__*/ createWatchContractEvent({
abi: dataHavenServiceManagerAbi,
eventName: 'SlashingComplete',
})
/**
* Wraps __{@link watchContractEvent}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `eventName` set to `"SnowbridgeGatewaySet"`
*/

162
test/suites/slash.test.ts Normal file
View file

@ -0,0 +1,162 @@
import { beforeAll, describe, expect, it } from "bun:test";
import { FixedSizeBinary } from "polkadot-api";
import { CROSS_CHAIN_TIMEOUTS, getPapiSigner, logger } from "utils";
import type { Address } from "viem";
import { BaseTestSuite } from "../framework";
import { getContractInstance, parseDeploymentsFile } from "../utils/contracts";
import { waitForDataHavenEvent } from "../utils/events";
class SlashTestSuite extends BaseTestSuite {
constructor() {
super({
suiteName: "slash"
});
// Set up hooks in constructor
this.setupHooks();
}
}
// Create the test suite instance
const suite = new SlashTestSuite();
describe("Should slash an operator", () => {
let publicClient: any;
let dhApi: any;
beforeAll(async () => {
const connectors = suite.getTestConnectors();
publicClient = connectors.publicClient;
dhApi = connectors.dhApi;
const deployments = await parseDeploymentsFile();
const strategyAddress =
deployments.DeployedStrategies?.[0]?.address ?? "0x0000000000000000000000000000000000000000";
logger.info(
"Setting slashing required parameters:\n" +
` StrategyAddress=${strategyAddress}\n` +
` ServiceManagerAddress=${deployments.ServiceManager}\n`
);
// Build sudo calls to set parameters
const calls = [
// Set DatahavenServiceManagerAddress address (required to call the slash operator function)
dhApi.tx.Parameters.set_parameter({
key_value: {
type: "RuntimeConfig",
value: {
type: "DatahavenServiceManagerAddress",
value: [new FixedSizeBinary(Buffer.from(deployments.ServiceManager.slice(2), "hex"))]
}
}
}).decodedCall,
// Set strategies and multipliers: [(strategy_address, multiplier)] (we use the same rewards strategy for the slashing logic)
dhApi.tx.Parameters.set_parameter({
key_value: {
type: "RuntimeConfig",
value: {
type: "RewardsStrategiesAndMultipliers",
value: [[[new FixedSizeBinary(Buffer.from(strategyAddress.slice(2), "hex")), 1n]]]
}
}
}).decodedCall
];
const tx = dhApi.tx.Sudo.sudo({
call: dhApi.tx.Utility.batch_all({ calls }).decodedCall
});
const alithSigner = getPapiSigner("ALITH");
const result = await tx.signAndSubmit(alithSigner);
if (!result.ok) {
throw new Error("Failed to set slasher required parameters");
}
logger.info("slashing required parameters set successfully");
});
it("verify we have the agent origin set", async () => {
const gateway = await getContractInstance("Gateway");
const serviceManager = await getContractInstance("ServiceManager");
expect(serviceManager.address).toBeDefined();
expect(gateway.address).toBeDefined();
const rewardsInitiator = (await publicClient.readContract({
address: serviceManager.address,
abi: serviceManager.abi,
functionName: "rewardsInitiator",
args: []
})) as Address;
// ServiceManager must have a rewardsInitiator configured for EigenLayer rewards submission
expect(rewardsInitiator).toBeDefined();
logger.info(`ServiceManager rewardsInitiator: ${rewardsInitiator}`);
});
it("Activate slashing", async () => {
const mode = await dhApi.query.ExternalValidatorsSlashes.SlashingMode.getValue();
expect(mode.type).toBe("LogOnly");
mode.type = "Enabled";
const sudoSlashCall = dhApi.tx.ExternalValidatorsSlashes.set_slashing_mode({
mode
});
const sudoTx = dhApi.tx.Sudo.sudo({
call: sudoSlashCall.decodedCall
});
const alithSigner = getPapiSigner("ALITH");
const result = await sudoTx.signAndSubmit(alithSigner);
expect(result.ok).toBeTruthy();
const mode2 = await dhApi.query.ExternalValidatorsSlashes.SlashingMode.getValue();
expect(mode2.type).toBe("Enabled");
}, 40000);
it("use sudo to slash operator", async () => {
// get era number
const activeEra = await dhApi.query.ExternalValidators.ActiveEra.getValue();
if (activeEra === undefined) {
throw new Error("couldn't get current era");
}
// need operator address to slash
const validator = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
const sudoSlashCall = dhApi.tx.ExternalValidatorsSlashes.force_inject_slash({
validator,
era: activeEra?.index || 0, // Will fail if active era is 0. !! Important !! Sometimes for the inject to work (because of some latency) we need to inject in the `activeEra.index + 1`
percentage: 20
});
const sudoTx = dhApi.tx.Sudo.sudo({
call: sudoSlashCall.decodedCall
});
const alithSigner = getPapiSigner("ALITH");
const resultSubmitTx = await sudoTx.signAndSubmit(alithSigner);
expect(resultSubmitTx.ok).toBeTruthy();
logger.info("Transaction submitted !");
// look for event inject event
const resultEventSlashInjected =
await dhApi.event.ExternalValidatorsSlashes.SlashInjected.pull();
if (resultEventSlashInjected.length === 0) {
throw new Error("SlashInjected event not found");
}
logger.info("Slash injected");
const resultEventSlashesMessageSent = await waitForDataHavenEvent<{ message_id: any }>({
api: dhApi,
pallet: "ExternalValidatorsSlashes",
event: "SlashesMessageSent",
timeout: CROSS_CHAIN_TIMEOUTS.DH_TO_ETH_MS
});
if (!resultEventSlashesMessageSent) {
throw new Error("SlashesMessageSent event not found");
}
logger.info("Slashes message sent");
}, 560000);
});