Skip to content
Merged
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/shepherd-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ tracing.workspace = true
# `capture_tracing` observes the keeper run's diagnostics in the
# acceptance tests.
nexum-sdk-test = { path = "../nexum-sdk-test" }
alloy-sol-types.workspace = true
proptest.workspace = true
# Dev-only cycle (this crate <- shepherd-sdk-test): cargo permits it,
# and the keeper run acceptance-tests against the composed MockHost
Expand Down
111 changes: 111 additions & 0 deletions crates/shepherd-sdk/src/cow/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//! CoW on-chain event ABIs, mirroring `shepherd:cow/cow-events`.
//!
//! `wit/shepherd-cow/cow-events.wit` is the package of record; the
//! constants here are parity-tested against it, the `cowprotocol`
//! `sol!` types, and each keeper's `module.toml`.

use alloy_primitives::{B256, b256};

/// One on-chain event surface: the canonical Solidity signature and
/// its keccak256 topic-0.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EventAbi {
/// Canonical Solidity event signature.
pub signature: &'static str,
/// keccak256 of [`Self::signature`]: the log's topic-0.
pub topic0: B256,
}

/// `ComposableCoW.ConditionalOrderCreated`.
pub const CONDITIONAL_ORDER_CREATED: EventAbi = EventAbi {
signature: "ConditionalOrderCreated(address,(address,bytes32,bytes))",
topic0: b256!("2cceac5555b0ca45a3744ced542f54b56ad2eb45e521962372eef212a2cbf361"),
};

/// `CoWSwapOnchainOrders.OrderPlacement` (EthFlow).
pub const ORDER_PLACEMENT: EventAbi = EventAbi {
signature: "OrderPlacement(address,(address,address,address,uint256,uint256,uint32,bytes32,\
uint256,bytes32,bool,bytes32,bytes32),(uint8,bytes),bytes)",
topic0: b256!("cf5f9de2984132265203b5c335b25727702ca77262ff622e136baa7362bf1da9"),
};

/// Every event surface the keepers decode.
pub const ALL: &[EventAbi] = &[CONDITIONAL_ORDER_CREATED, ORDER_PLACEMENT];

#[cfg(test)]
mod tests {
use alloy_primitives::keccak256;
use alloy_sol_types::SolEvent;
use cowprotocol::{CoWSwapOnchainOrders, ComposableCoW};

use super::*;

#[test]
fn topic0_is_keccak_of_signature() {
for abi in ALL {
assert_eq!(abi.topic0, keccak256(abi.signature), "{}", abi.signature);
}
}

#[test]
fn matches_the_sol_decoder_types() {
assert_eq!(
ComposableCoW::ConditionalOrderCreated::SIGNATURE,
CONDITIONAL_ORDER_CREATED.signature,
);
assert_eq!(
ComposableCoW::ConditionalOrderCreated::SIGNATURE_HASH,
CONDITIONAL_ORDER_CREATED.topic0,
);
assert_eq!(
CoWSwapOnchainOrders::OrderPlacement::SIGNATURE,
ORDER_PLACEMENT.signature,
);
assert_eq!(
CoWSwapOnchainOrders::OrderPlacement::SIGNATURE_HASH,
ORDER_PLACEMENT.topic0,
);
}

#[test]
fn wit_package_of_record_pins_every_surface() {
let wit = include_str!("../../../../wit/shepherd-cow/cow-events.wit");
let flat: String = wit
.lines()
.map(|l| l.trim().trim_start_matches("/// "))
.collect();
for abi in ALL {
assert!(
flat.contains(abi.signature),
"cow-events.wit must pin the signature {}",
abi.signature,
);
assert!(
flat.contains(&format!("{:#x}", abi.topic0)),
"cow-events.wit must pin the topic-0 {:#x}",
abi.topic0,
);
}
}

/// Layering gate: no generic WIT package references `shepherd:cow`.
#[test]
fn generic_wit_packages_never_reference_shepherd_cow() {
let wit_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../wit");
for pkg in std::fs::read_dir(&wit_root).expect("wit dir") {
let pkg = pkg.expect("wit dir entry").path();
if pkg.file_name().is_some_and(|n| n == "shepherd-cow") {
continue;
}
for file in std::fs::read_dir(&pkg).expect("wit package dir") {
let path = file.expect("wit package entry").path();
let text = std::fs::read_to_string(&path).expect("read wit file");
assert!(
!text.contains("shepherd:cow"),
"{} references shepherd:cow",
path.display(),
);
}
}
}
}
1 change: 1 addition & 0 deletions crates/shepherd-sdk/src/cow/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//! keeper run is generic over the host traits alone.

pub mod error;
pub mod events;
pub mod order;
pub mod run;

Expand Down
3 changes: 2 additions & 1 deletion docs/deployment/multi-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@ event_signature = "0xcf5f9de2984132265203b5c335b25727702ca77262ff622e136baa7362b
## Event topic reference

These are keccak256 hashes of the event signatures. They are the same on every
chain; only the contract `address` changes for EthFlow.
chain; only the contract `address` changes for EthFlow. Package of record:
`wit/shepherd-cow/cow-events.wit`.

| Event | Topic-0 |
|-------|---------|
Expand Down
4 changes: 3 additions & 1 deletion modules/ethflow-watcher/module.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ allow = []

# CoWSwapEthFlow.OrderPlacement on Sepolia. topic-0 = keccak256(
# "OrderPlacement(address,(address,address,address,uint256,uint256,uint32,
# bytes32,uint256,bytes32,bool,bytes32,bytes32),(uint8,bytes),bytes)").
# bytes32,uint256,bytes32,bool,bytes32,bytes32),(uint8,bytes),bytes)"),
# pinned in wit/shepherd-cow/cow-events.wit and parity-tested in
# strategy.rs.
# `address` is the Sepolia ETH_FLOW_PRODUCTION deployment from
# `cowprotocol/ethflowcontract/networks.prod.json`. Unlike
# ComposableCoW's CREATE2 address, EthFlow has had multiple per-network
Expand Down
27 changes: 15 additions & 12 deletions modules/ethflow-watcher/src/strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ use cowprotocol::{
use nexum_sdk::events::Log;
use nexum_sdk::host::Fault;
use nexum_sdk::keeper::Journal;
use shepherd_sdk::cow::{CowApiError, CowHost, gpv2_to_order_data};
use shepherd_sdk::cow::{CowApiError, CowHost, events, gpv2_to_order_data};

/// Decoded payload of a `CoWSwapOnchainOrders.OrderPlacement` log.
/// `GPv2OrderData` is ~300 bytes; box it so the struct stays
Expand Down Expand Up @@ -89,13 +89,16 @@ pub fn on_chain_logs<H: CowHost>(host: &H, chain_id: u64, logs: &[Log]) -> Resul
/// `ETH_FLOW_STAGING` (defensive - the host's `[[subscription]]`
/// filter already pins the address, but a misconfigured engine could
/// still leak through);
/// - topic0 does not match the event signature; or
/// - topic-0 does not match the `shepherd:cow/cow-events` pin; or
/// - the ABI body fails to decode.
pub(crate) fn decode_order_placement(log: &Log) -> Option<DecodedPlacement> {
let contract = log.address();
if contract != ETH_FLOW_PRODUCTION && contract != ETH_FLOW_STAGING {
return None;
}
if log.topics().first() != Some(&events::ORDER_PLACEMENT.topic0) {
return None;
}
let decoded = OrderPlacement::decode_log(&log.inner).ok()?;
Some(DecodedPlacement {
contract,
Expand Down Expand Up @@ -178,7 +181,7 @@ fn compute_uid(chain_id: u64, placement: &DecodedPlacement) -> Option<OrderUid>
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::{U256, address, b256, hex};
use alloy_primitives::{U256, address, hex};
use alloy_sol_types::SolValue;
use cowprotocol::{BuyTokenDestination, OnchainSigningScheme, OrderKind, SellTokenSource};
use nexum_sdk::Level;
Expand Down Expand Up @@ -495,29 +498,29 @@ mod tests {
);
}

/// Guard: the topic-0 hardcoded in `module.toml` matches the
/// keccak256 of the canonical `OrderPlacement` signature.
/// A typo or ABI drift would silently miss every EthFlow event.
/// Guard: the `sol!` decoder's topic-0 matches the
/// `shepherd:cow/cow-events` package of record. A typo or ABI
/// drift would silently miss every EthFlow event.
#[test]
fn topic0_matches_order_placement_canonical_signature() {
assert_eq!(
OrderPlacement::SIGNATURE_HASH,
b256!("cf5f9de2984132265203b5c335b25727702ca77262ff622e136baa7362bf1da9"),
"module.toml event_signature must equal keccak256 of the canonical ABI signature",
events::ORDER_PLACEMENT.topic0,
"sol! topic-0 must match the shepherd:cow/cow-events pin",
);
}

/// Stronger guard than the constant check above: read the shipped
/// `module.toml` and assert its pinned `event_signature` actually
/// equals `OrderPlacement::SIGNATURE_HASH` - catches a manifest/code
/// drift the ABI-hash assertion cannot see. (Ported from #164.)
/// equals the package-of-record topic-0 - catches a manifest/code
/// drift the decoder assertion cannot see.
#[test]
fn manifest_topic0_matches_order_placement_signature_hash() {
let manifest = include_str!("../module.toml");
let expected = format!("0x{:x}", OrderPlacement::SIGNATURE_HASH);
let expected = format!("{:#x}", events::ORDER_PLACEMENT.topic0);
assert!(
manifest.contains(&expected),
"module.toml event_signature must equal OrderPlacement::SIGNATURE_HASH ({expected})",
"module.toml event_signature must equal the shepherd:cow/cow-events pin ({expected})",
);
}

Expand Down
7 changes: 4 additions & 3 deletions modules/twap-monitor/module.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ allow = []
# --- subscriptions ------------------------------------------------------

# ComposableCoW.ConditionalOrderCreated emissions on Sepolia. topic-0 =
# keccak256("ConditionalOrderCreated(address,(address,bytes32,bytes))").
# Both `address` and `event_signature` are pinned so the supervisor
# does not deliver unrelated logs to the module.
# keccak256("ConditionalOrderCreated(address,(address,bytes32,bytes))"),
# pinned in wit/shepherd-cow/cow-events.wit and parity-tested in
# strategy.rs. Both `address` and `event_signature` are pinned so the
# supervisor does not deliver unrelated logs to the module.
[[subscription]]
kind = "chain-log"
chain_id = 11155111
Expand Down
24 changes: 15 additions & 9 deletions modules/twap-monitor/src/strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use nexum_sdk::chain::{eth_call_params, parse_eth_call_result};
use nexum_sdk::events::Log;
use nexum_sdk::host::{ChainError, Fault};
use nexum_sdk::keeper::{ConditionalSource, Tick, WatchRef, WatchSet};
use shepherd_sdk::cow::{CowHost, run};
use shepherd_sdk::cow::{CowHost, events, run};

/// Block fields the poll path reads on every dispatch.
pub struct BlockInfo {
Expand Down Expand Up @@ -84,7 +84,12 @@ pub fn on_block<H: CowHost>(host: &H, block: BlockInfo) -> Result<(), Fault> {

// ---- indexing path ----

/// Topic-0 resolves from the `shepherd:cow/cow-events` package of
/// record before the ABI decode.
fn decode_conditional_order_created(log: &Log) -> Option<(Address, ConditionalOrderParams)> {
if log.topics().first() != Some(&events::CONDITIONAL_ORDER_CREATED.topic0) {
return None;
}
let decoded = ConditionalOrderCreated::decode_log(&log.inner).ok()?;
Some((decoded.data.owner, decoded.data.params))
}
Expand Down Expand Up @@ -800,28 +805,29 @@ mod tests {
});
}

/// Guard: the topic-0 hardcoded in `module.toml` matches the
/// keccak256 of the canonical `ConditionalOrderCreated` signature.
/// A typo or ABI drift would silently miss every registration event.
/// Guard: the `sol!` decoder's topic-0 matches the
/// `shepherd:cow/cow-events` package of record. A typo or ABI
/// drift would silently miss every registration event.
#[test]
fn topic0_matches_conditional_order_created_canonical_signature() {
assert_eq!(
ConditionalOrderCreated::SIGNATURE_HASH,
b256!("2cceac5555b0ca45a3744ced542f54b56ad2eb45e521962372eef212a2cbf361"),
"module.toml event_signature must equal keccak256 of the canonical ABI signature",
events::CONDITIONAL_ORDER_CREATED.topic0,
"sol! topic-0 must match the shepherd:cow/cow-events pin",
);
}

/// Stronger guard than the constant check above: read the shipped
/// `module.toml` and assert its pinned `event_signature` actually
/// equals `ConditionalOrderCreated::SIGNATURE_HASH`. (Ported from #164.)
/// equals the package-of-record topic-0 - catches a manifest/code
/// drift the decoder assertion cannot see.
#[test]
fn manifest_topic0_matches_conditional_order_created_signature_hash() {
let manifest = include_str!("../module.toml");
let expected = format!("0x{:x}", ConditionalOrderCreated::SIGNATURE_HASH);
let expected = format!("{:#x}", events::CONDITIONAL_ORDER_CREATED.topic0);
assert!(
manifest.contains(&expected),
"module.toml event_signature must equal ConditionalOrderCreated::SIGNATURE_HASH ({expected})",
"module.toml event_signature must equal the shepherd:cow/cow-events pin ({expected})",
);
}
}
2 changes: 2 additions & 0 deletions wit/shepherd-cow/cow-api.wit
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package shepherd:cow@0.1.0;

/// Legacy host-extension surface: retiring. Submission moves to the
/// generic videre pool seam; deleted at the fork-gated poll wire-swap.
interface cow-api {
use nexum:host/types@0.1.0.{chain-id, fault};

Expand Down
20 changes: 20 additions & 0 deletions wit/shepherd-cow/cow-events.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package shepherd:cow@0.1.0;

/// CoW on-chain event surfaces the keepers decode. Package of record
/// for the canonical event signatures and their keccak256 topic-0
/// hashes; guest constants and module manifests are parity-tested
/// against this file.
interface cow-events {
/// A decoded CoW on-chain event surface. Each variant doc pins the
/// canonical Solidity signature and its topic-0.
enum cow-event {
/// ComposableCoW registration.
/// signature: ConditionalOrderCreated(address,(address,bytes32,bytes))
/// topic0: 0x2cceac5555b0ca45a3744ced542f54b56ad2eb45e521962372eef212a2cbf361
conditional-order-created,
/// CoWSwapOnchainOrders (EthFlow) placement.
/// signature: OrderPlacement(address,(address,address,address,uint256,uint256,uint32,bytes32,uint256,bytes32,bool,bytes32,bytes32),(uint8,bytes),bytes)
/// topic0: 0xcf5f9de2984132265203b5c335b25727702ca77262ff622e136baa7362bf1da9
order-placement,
}
}
1 change: 1 addition & 0 deletions wit/shepherd-cow/cow-ext.wit
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package shepherd:cow@0.1.0;
/// Extension world: the cow-api interface alone, wired into a module
/// linker by the cow extension. Kept separate from `shepherd` so the
/// extension contributes only its own import, never the core interfaces.
/// Retiring with `cow-api`.
world cow-ext {
import cow-api;
}
Loading