Skip to content
Closed
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
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,14 @@ skills-lock.json

# Engine runtime state (default state_dir from engine.toml).
data/

# E2E automation: rendered configs with embedded RPC keys + script state
# never get committed.
*.local.toml
scripts/.state
scripts/.env

# Generated reports under e2e-reports/ (operator commits the filled-in ones
# manually via `git add -f`).
docs/operations/e2e-reports/engine-*.log
docs/operations/e2e-reports/metrics-*.txt
322 changes: 322 additions & 0 deletions docs/operations/e2e-cow-1064-prep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
# E2E COW-1064 run-prep punch list

Companion to `docs/operations/e2e-testnet-runbook.md`. This file
captures every **pinned value** for the 2026-06-18 dry run of the
COW-1064 E2E so the operator can copy-paste through the on-chain
actions without re-deriving any UID, address, or calldata.

If you are running a *later* COW-1064 (different EOA, different
Safe, different config), do not reuse the UIDs / calldatas — they
are a function of all the pinned config below. Either re-derive
via the Python recipes in this doc, or re-run
`cargo test -p stop-loss --lib cow_1064` to lock the new UID.

---

## 0. Pinned identities (2026-06-18 run)

| Role | Address | Network | Notes |
|---|---|---|---|
| Test EOA | `0x7bF140727D27ea64b607E042f1225680B40ECa6A` | Sepolia | Bruno-controlled. Funds itself via faucet. |
| Test Safe (single-sig, threshold 1) | `0x14995a1118Caf95833e923faf8Dd155721cd53c2` | Sepolia | EOA is the sole owner. Submits TWAP order. |
| ComposableCoW | `0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74` | Sepolia | Where `create((address,bytes32,bytes),bool)` lands. |
| TWAP handler | `0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5` | Sepolia | `ConditionalOrderParams.handler`. |
| CoWSwapEthFlow | `0xbA3cB449bD2B4ADddBc894D8697F5170800EAdeC` | Sepolia | EthFlow's production deployment; emits `OrderPlacement`. |
| GPv2Settlement | `0x9008D19f58AAbD9eD0D60971565AA8510560ab41` | Sepolia | `setPreSignature(orderUid, signed)` lives here. |
| GPv2VaultRelayer | `0xc92e8bdf79f0507f65a392b0ab4667716bfe0110` | Sepolia | Spender for sell-token ERC-20 approvals. |
| WETH9 | `0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14` | Sepolia | `deposit()` payable wraps ETH; `balanceOf(EOA)` is the sell-side balance. |
| COW Token | `0x0625aFB445C3B6B7B929342a04A22599fd5dBB59` | Sepolia | name="CoW Protocol Token", symbol="COW", decimals=18. |
| GPv2 domain separator | `0xdaee378bd0eb30ddf479272accf91761e697bc00e067a268f95f1d2732ed230b` | Sepolia | EIP-712 domain digest queried from chain. |

All addresses verified via `eth_getCode > 0` on
`https://ethereum-sepolia-rpc.publicnode.com` as of run prep.

---

## 1. Per-module config pinning

### stop-loss

`modules/examples/stop-loss/module.toml` is checked in on the
`feat/e2e-run-config-cow-1064` branch with the production-ready
config for this run. Effective values:

| Field | Value | Notes |
|---|---|---|
| `oracle_address` | `0x694AA1769357215DE4FAC081bf1f309aDC325306` | Chainlink ETH/USD Sepolia. |
| `decimals` | `8` | Chainlink USD-pair convention. |
| `trigger_price` | `2000.00` | Above the live Sepolia mocked answer (~$1681), `direction=below` → triggers on first block. |
| `owner` | `0x7bF1...Ca6A` | Test EOA. |
| `sell_token` | `0xfFf9...6B14` | WETH9 Sepolia. |
| `buy_token` | `0x0625...BB59` | COW Sepolia. |
| `sell_amount_wei` | `5000000000000000` | 0.005 WETH. |
| `buy_amount_wei` | `20000000000000000000` | 20 COW. Conservative quote at run-prep time. |
| `valid_to_seconds` | `4294967295` | uint32::MAX. |

### Resulting OrderUid

The strategy's `build_creation` is pinned by the
`cow_1064_e2e_settings_yield_expected_uid` regression test
(`crates/.../stop-loss/src/strategy.rs`). The canonical UID:

```
0xc2b9cb4ea1ee5a86d8049ac09d8f494bf04cca0a68407285f31e2e6379800be87bf140727d27ea64b607e042f1225680b40eca6affffffff
```

Decomposition (per `packOrderUidParams`):

| Offset | Bytes | Field | Value |
|---|---|---|---|
| 0..32 | 32 | `orderDigest` (EIP-712) | `0xc2b9cb4ea1ee5a86d8049ac09d8f494bf04cca0a68407285f31e2e6379800be8` |
| 32..52 | 20 | `owner` | `0x7bf140727d27ea64b607e042f1225680b40eca6a` |
| 52..56 | 4 | `validTo` (uint32) | `0xffffffff` |

### balance-tracker

Pinned to the EOA + Safe so the run sees ETH-balance diffs:

| Field | Value |
|---|---|
| `addresses` | `0x7bF1...Ca6A,0x1499...53c2` |
| `change_threshold` | `1000000000000000` (0.001 ETH) |

---

## 2. On-chain actions for the run window

> Order: action 1 can be done at any time before/during the run.
> Actions 2-4 should fire **after** the engine prints
> `INFO supervisor ready modules=5 chains=1` so the modules
> observe the events. They are independent; do them in any order.

### Action 1 (optional, pre-run): wrap 0.01 ETH → 0.01 WETH

Without WETH, stop-loss will hit `TransferSimulationFailed` ->
`backoff:` write (which is itself a valid terminal-marker per
the COW-1064 acceptance bar). To get the **`submitted:`** path,
wrap first then do action 2.

- Etherscan: https://sepolia.etherscan.io/address/0xfff9976782d46cc05630d1f6ebab18b2324d6b14#writeContract
- Connect Web3 from the EOA in Metamask
- Function `deposit` → payable value `0.01` ETH → Write

Verify: `balanceOf(EOA)` returns `10000000000000000` post-tx.

### Action 2 (optional, only if action 1 done): pre-sign stop-loss order

- Etherscan: https://sepolia.etherscan.io/address/0x9008d19f58aabd9ed0d60971565aa8510560ab41#writeProxyContract
- Connect Web3 from the EOA
- Function `setPreSignature(bytes orderUid, bool signed)`:
- `orderUid`:
```
0xc2b9cb4ea1ee5a86d8049ac09d8f494bf04cca0a68407285f31e2e6379800be87bf140727d27ea64b607e042f1225680b40eca6affffffff
```
- `signed`: `true`
- Write

Also approve WETH → GPv2VaultRelayer so the settle path is real:

- Etherscan: https://sepolia.etherscan.io/address/0xfff9976782d46cc05630d1f6ebab18b2324d6b14#writeContract
- Function `approve(address guy, uint256 wad)`:
- `guy`: `0xc92e8bdf79f0507f65a392b0ab4667716bfe0110`
- `wad`: `5000000000000000` (0.005 WETH — matches the order's sell_amount)
- Write

### Action 3: TWAP conditional order via Safe TX Builder

Triggers `ConditionalOrderCreated` → twap-monitor writes
`watch:{orderHash}`. The Safe pays the gas (~0.003 ETH); the
order will TRY to settle later but the Safe holds no WETH so
settlement will fail. **That's fine** — only the `create()`
event is required for the acceptance marker.

- Safe app: https://app.safe.global/transactions/queue?safe=sep:0x14995a1118Caf95833e923faf8Dd155721cd53c2
- New transaction → Transaction Builder
- Enter contract address: `0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74`
- Toggle "Use custom data (hex encoded)" ON
- Custom data:

```
0x6bfae1ca000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006cf1e9ca41f7611def408122793c358a3d11e5a5000000000000000000000000000000000000000000000000000000006670f00000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000140000000000000000000000000fff9976782d46cc05630d1f6ebab18b2324d6b140000000000000000000000000625afb445c3b6b7b929342a04a22599fd5dbb5900000000000000000000000014995a1118caf95833e923faf8dd155721cd53c200000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000006f05b59d3b2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000025800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
```

(516 bytes — the `create(ConditionalOrderParams, bool dispatch)`
call with a 2-part TWAP from WETH → COW, 0.001 WETH per part,
600 s between parts, salt pinned to `0x...6670f000`.)

- ETH value: `0`
- Create batch → Send batch → sign with the EOA

Expected log within 1-2 Sepolia blocks:

```
INFO twap-monitor watch:0x<orderHash> chain_id=11155111
```

### Action 4: EthFlow swap via cow-swap UI

Triggers `OrderPlacement` → ethflow-watcher writes
`submitted:{uid}` (or `dropped:{uid}` if the orderbook rejects;
both are valid terminal markers).

Easiest path is the cow-swap UI:

1. https://swap.cow.fi/#/11155111/swap/ETH/COW (Sepolia)
2. Connect Metamask, EOA selected, network=Sepolia
3. Sell amount: `0.005` ETH
4. Click "Swap" → it builds the EthFlow `createOrder` tx
5. Approve in Metamask

The UI handles `quoteId` resolution + `appData` IPFS pinning +
EthFlow contract call. Sell amount is small enough to fit in the
~0.05 ETH budget plus gas.

Expected log within 1-2 Sepolia blocks:

```
INFO ethflow-watcher submitted:0x<uid>
```

If the UI errors out (Sepolia orderbook can be flaky), fallback
to calling EthFlow directly via Etherscan:

- https://sepolia.etherscan.io/address/0xba3cb449bd2b4adddbc894d8697f5170800eadec#writeContract
- Function `createOrder((address,address,uint256,uint256,bytes32,uint256,uint32,bool,int64))`
- The shape of the tuple needs the orderbook quote endpoint hit
first to get `feeAmount` + `quoteId` — easier to defer to the
UI for the run.

---

## 3. Validation snippets for the operator

Run these in a separate shell while the engine is up:

```bash
RPC="wss://eth-sepolia.g.alchemy.com/v2/<YOUR_KEY>" # replace
EOA="0x7bF140727D27ea64b607E042f1225680B40ECa6A"
SAFE="0x14995a1118Caf95833e923faf8Dd155721cd53c2"
WETH="0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14"

# EOA + Safe balances
cast balance $EOA --rpc-url $RPC
cast balance $SAFE --rpc-url $RPC

# EOA WETH balance + GPv2VaultRelayer allowance
cast call $WETH "balanceOf(address)(uint256)" $EOA --rpc-url $RPC
cast call $WETH "allowance(address,address)(uint256)" \
$EOA 0xc92e8bdf79f0507f65a392b0ab4667716bfe0110 --rpc-url $RPC

# Did setPreSignature land?
cast call 0x9008D19f58AAbD9eD0D60971565AA8510560ab41 \
"preSignature(bytes)(uint256)" \
0xc2b9cb4ea1ee5a86d8049ac09d8f494bf04cca0a68407285f31e2e6379800be87bf140727d27ea64b607e042f1225680b40eca6affffffff \
--rpc-url $RPC
# Returns 1 if pre-signed, 0 otherwise.

# Mine the supervisor log for terminal markers in real time
journalctl -u shepherd -f --output=json \
| jq -r '.MESSAGE | fromjson? | select(.fields.message | test("watch:|submitted:|dropped:|backoff:|TRIGGERED")) | "\(.fields.module): \(.fields.message)"'
```

(If you don't have `cast` installed: `curl -L https://foundry.paradigm.xyz | bash && foundryup`.)

---

## 4. Recipes for re-deriving the pinned values

If anything in section 0 drifts, regenerate from these recipes.

### 4.1 OrderUid

Either:

```bash
cargo test -p stop-loss --lib cow_1064 -- --nocapture
```

(asserts against the same constants pinned in `module.toml`,
fails loudly if the EIP-712 type-hash or domain separator
shifts).

Or with raw Python:

```python
from eth_utils import keccak

# Replace these 8 values to re-derive
DOMAIN_SEP = bytes.fromhex("daee378bd0eb30ddf479272accf91761e697bc00e067a268f95f1d2732ed230b")
SELL_TOKEN = bytes.fromhex("fFf9976782d46CC05630D1f6eBAb18b2324d6B14")
BUY_TOKEN = bytes.fromhex("0625aFB445C3B6B7B929342a04A22599fd5dBB59")
OWNER = bytes.fromhex("7bF140727D27ea64b607E042f1225680B40ECa6A")
RECEIVER = OWNER
SELL_AMOUNT = 5_000_000_000_000_000
BUY_AMOUNT = 20_000_000_000_000_000_000
VALID_TO = 4_294_967_295

APP_DATA = bytes.fromhex("b48d38f93eaa084033fc5970bf96e559c33c4cdc07d889ab00b4d63f9590739d") # keccak("{}")
KIND_SELL = keccak(b"sell")
ERC20 = keccak(b"erc20")
TYPE_HASH = keccak(b"Order(address sellToken,address buyToken,address receiver,uint256 sellAmount,uint256 buyAmount,uint32 validTo,bytes32 appData,uint256 feeAmount,string kind,bool partiallyFillable,string sellTokenBalance,string buyTokenBalance)")
pad32 = lambda b: bytes(32-len(b)) + b
uint = lambda v: v.to_bytes(32, "big")
struct_hash = keccak(
TYPE_HASH + pad32(SELL_TOKEN) + pad32(BUY_TOKEN) + pad32(RECEIVER)
+ uint(SELL_AMOUNT) + uint(BUY_AMOUNT) + uint(VALID_TO)
+ APP_DATA + uint(0) + KIND_SELL
+ b"\x00"*32 + ERC20 + ERC20 # partiallyFillable=false
)
order_digest = keccak(b"\x19\x01" + DOMAIN_SEP + struct_hash)
uid = order_digest + OWNER + VALID_TO.to_bytes(4, "big")
print("0x" + uid.hex())
```

### 4.2 ComposableCoW.create() calldata

```python
from eth_utils import keccak
from eth_abi import encode

selector = keccak(b"create((address,bytes32,bytes),bool)")[:4]
# Edit these 10 fields to retarget the TWAP
static = encode(
["(address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes32)"],
[(
"0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14", # sellToken
"0x0625aFB445C3B6B7B929342a04A22599fd5dBB59", # buyToken
"0x14995a1118Caf95833e923faf8Dd155721cd53c2", # receiver
1_000_000_000_000_000, 500_000_000_000_000_000, # partSellAmount, minPartLimit
0, 2, 600, 0, # t0, n, t, span
b"\x00" * 32, # appData
)]
)
calldata = selector + encode(
["(address,bytes32,bytes)", "bool"],
[(
"0x6cF1e9cA41f7611dEf408122793c358a3d11E5a5", # TWAP handler
bytes.fromhex("000000000000000000000000000000000000000000000000000000006670f000"), # salt
static,
), True]
)
print("0x" + calldata.hex())
```

---

## 5. Acceptance checklist for THIS run

Hand-check at the end of the run (also goes in
`e2e-report-YYYY-MM-DD.md` section 7):

- [ ] EOA at `0x7bF1...Ca6A` still has ≥ 0.03 ETH remaining
- [ ] twap-monitor logged `watch:0x...` after action 3
- [ ] ethflow-watcher logged `submitted:0x...` after action 4
- [ ] stop-loss logged `backoff:` or `TRIGGERED + submitted:` (depending on whether action 1+2 ran)
- [ ] price-alert logged `TRIGGERED` on first block
- [ ] balance-tracker logged a `last:0x7bf1...` write on first block + at least one Warn diff log over the run window
- [ ] `shepherd_module_poisoned{...} == 0` for all 5 modules at end
- [ ] `shepherd_module_errors_total{error_kind="trap"} == 0` for all modules
- [ ] ≥ 1500 Sepolia blocks dispatched (`block delta` in report section 2)

If all green: COW-1064 closes, COW-1031 7-day soak can start
on the same code.
16 changes: 13 additions & 3 deletions modules/examples/balance-tracker/module.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ chain_id = 11155111
[config]
# Comma-separated list of 0x-prefixed 20-byte addresses. Whitespace
# around entries is tolerated.
addresses = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8,0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
# Change threshold in wei. Default is 0.1 ETH = 10**17.
change_threshold = "100000000000000000"
#
# COW-1064 E2E pinning: the test EOA + Safe co-located on Sepolia.
# Both fund themselves during the runbook's prep step (EOA via
# faucet, Safe via EOA send), so balance-tracker writes
# `last:0x7bf1...` and `last:0x1499...` on the first dispatch and
# logs a Warn diff if the Safe later receives the TWAP order's
# gas-side transfer (or any subsequent move >= change_threshold).
addresses = "0x7bF140727D27ea64b607E042f1225680B40ECa6A,0x14995a1118Caf95833e923faf8Dd155721cd53c2"
# Change threshold in wei. Lowered from 0.1 ETH to 0.001 ETH so the
# E2E run's small on-chain actions (wrap-to-WETH ~0.005 ETH gas cost,
# TWAP Safe call ~0.003 ETH gas) show as diffs in the Warn log;
# production deploys leave this at 0.1 ETH or higher.
change_threshold = "1000000000000000"
34 changes: 27 additions & 7 deletions modules/examples/stop-loss/module.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,34 @@ chain_id = 11155111 # Sepolia
oracle_address = "0x694AA1769357215DE4FAC081bf1f309aDC325306"
# Oracle's decimals (Chainlink USD pairs are 8).
decimals = "8"
# Trigger price in the oracle's native decimal units. Below this, sell.
trigger_price = "2500.00"
# Trigger price in the oracle's native decimal units. The Sepolia
# Chainlink ETH/USD feed reports a mocked value around $1681 at the
# time of the COW-1064 E2E run (2026-06-18). Setting the trigger
# *above* the live price + direction=below ensures the strategy fires
# on the first block.
trigger_price = "2000.00"
# Order parameters. The owner pre-signs via GPv2Signing.setPreSignature
# (on-chain, outside this module); the module submits the body with
# Signature::PreSign on trigger.
owner = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
sell_token = "0x6810e776880C02933D47DB1b9fc05908e5386b96"
buy_token = "0xfff9976782d46cc05630d1f6ebab18b2324d6b14"
sell_amount_wei = "1000000000000000000"
buy_amount_wei = "300000000000000000"
#
# E2E run pinning (COW-1064): test EOA on Sepolia with 0.05 ETH
# balance. Without a pre-sign + a WETH wrap the orderbook will reject
# with TransferSimulationFailed which the SDK classifies as
# TryNextBlock — that itself is a valid terminal marker (`backoff:`
# write to local-store) and proves the full submit path E2E.
owner = "0x7bF140727D27ea64b607E042f1225680B40ECa6A"
# WETH9 Sepolia (`wss://sepolia.etherscan.io/token/0xfff9976782d46cc05630d1f6ebab18b2324d6b14`).
sell_token = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14"
# COW token Sepolia (verified on-chain: name="CoW Protocol Token",
# symbol="COW", decimals=18).
buy_token = "0x0625aFB445C3B6B7B929342a04A22599fd5dBB59"
# 0.005 WETH (small enough to fit in the 0.01 WETH wrap budget the
# E2E runbook recommends; large enough that the orderbook's min-
# quote endpoint actually returns a price).
sell_amount_wei = "5000000000000000"
# 20 COW (conservative; current quote on cow.fi/sepolia at the time
# of the E2E run is ~30 COW per 0.005 WETH so a 20 COW buy_amount
# leaves room for slippage without making the order too generous).
buy_amount_wei = "20000000000000000000"
# uint32::MAX = order never expires.
valid_to_seconds = "4294967295"
Loading