Skip to main content

degenbot_executor/
config.rs

1//! The `execute(commands, config)` ABI `config` `uint256` packing.
2//!
3//! Ports `examples/cmd_stream.py::pack_config` (L137–L171). Mirrors the
4//! `cmd_executor.vy` `execute` config-bitfield layout:
5//!
6//! - bits `0–7`:   `check_mode` (0=skip, 1=WETH+ETH, 2=ERC6909 WETH)
7//! - bits `8–23`:  `bribe_bips` (0=none, 1–10000=basis points)
8//! - bits `24–31`: `bribe_recipient_idx` (0=block.coinbase, 1–31=table idx)
9//! - bits `32–255`: `expected_value` (pre-tx balance for the selected mode)
10//!
11//! Bribes moved out of the command stream (opcodes 0x02/0x03 reserved) into
12//! this parameter; pass the result as the 2nd arg to `execute(commands, config)`.
13
14use alloy::primitives::U256;
15
16use crate::encoders::MAX_INDEXED_ADDRESSES;
17
18/// Max bribe in basis points (100% = 10000 bips).
19pub const MAX_BRIBE_BIPS: u16 = 10_000;
20
21/// Max `check_mode` selector (0=skip, 1=WETH+ETH, 2=ERC6909 WETH, 3=SWEEP).
22pub const MAX_CHECK_MODE: u8 = 3;
23
24/// Error raised by an out-of-range config field.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
26pub enum ConfigError {
27    /// `check_mode` not in `0..=3`.
28    #[error("check_mode must be 0–3, got {0}")]
29    CheckModeOutOfRange(u8),
30    /// `bribe_bips` not in `0..=10000`.
31    #[error("bribe_bips must be 0–10000, got {0}")]
32    BribeBipsOutOfRange(u16),
33    /// `bribe_recipient_idx` not in `0..32`.
34    #[error("bribe_recipient_idx must be 0–31, got {0}")]
35    BribeRecipientOutOfRange(u8),
36}
37
38/// Pack the `execute()` `config` ABI parameter.
39///
40/// Mirrors `examples/cmd_stream.py::pack_config`. When `bribe_bips > 0`,
41/// `expected_value` MUST be the real pre-tx balance — the contract computes
42/// `profit = combined_after - expected_value`, so `expected_value=0` with a
43/// bribe over-bribes (drains the full balance).
44///
45/// # Errors
46///
47/// Returns [`Err`] for any out-of-range field (`check_mode > 3`,
48/// `bribe_bips > 10000`, `bribe_recipient_idx ≥ MAX_INDEXED_ADDRESSES (32)`).
49pub fn pack_config(
50    check_mode: u8,
51    expected_value: U256,
52    bribe_bips: u16,
53    bribe_recipient_idx: u8,
54) -> Result<U256, ConfigError> {
55    if check_mode > MAX_CHECK_MODE {
56        return Err(ConfigError::CheckModeOutOfRange(check_mode));
57    }
58    if bribe_bips > MAX_BRIBE_BIPS {
59        return Err(ConfigError::BribeBipsOutOfRange(bribe_bips));
60    }
61    // `MAX_INDEXED_ADDRESSES` (32) fits in `u8`; the `<` makes the bound
62    // `0..=31` (block.coinbase=0, table idxs 1..31).
63    if bribe_recipient_idx as usize >= MAX_INDEXED_ADDRESSES {
64        return Err(ConfigError::BribeRecipientOutOfRange(bribe_recipient_idx));
65    }
66
67    // (expected_value << 32) | (bribe_recipient_idx << 24) | (bribe_bips << 8) | check_mode
68    Ok((expected_value << U256::from(32u64))
69        | (U256::from(bribe_recipient_idx) << U256::from(24u64))
70        | (U256::from(bribe_bips) << U256::from(8u64))
71        | U256::from(check_mode))
72}
73
74#[cfg(test)]
75#[expect(clippy::unwrap_used)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn pack_default() {
81        // expected_value=0, all-zero fields → 0.
82        assert_eq!(pack_config(0, U256::ZERO, 0, 0).unwrap(), U256::ZERO);
83    }
84
85    #[test]
86    fn pack_check_mode_only() {
87        // check_mode=1, everything else zero → 1.
88        assert_eq!(pack_config(1, U256::ZERO, 0, 0).unwrap(), U256::from(1u64));
89    }
90
91    #[test]
92    fn pack_bribe_bips_in_byte_2_3() {
93        // bribe_bips=100 → (100 << 8) = 25600.
94        assert_eq!(
95            pack_config(0, U256::ZERO, 100, 0).unwrap(),
96            U256::from(25_600u64)
97        );
98    }
99
100    #[test]
101    fn pack_recipient_in_byte_3() {
102        // recipient_idx=5 → (5 << 24) = 83886080.
103        assert_eq!(
104            pack_config(0, U256::ZERO, 0, 5).unwrap(),
105            U256::from(83_886_080u64)
106        );
107    }
108
109    #[test]
110    fn pack_expected_value_high_bits() {
111        // expected_value=0x12345 → shifted left 32 bits.
112        let v = U256::from(0x12345u64);
113        let packed = pack_config(0, v, 0, 0).unwrap();
114        assert_eq!(packed, v << U256::from(32u64));
115    }
116
117    #[test]
118    fn pack_all_fields_compose() {
119        // check_mode=2, bribe_bips=500, recipient=3, expected=0xABC
120        let expected = U256::from(0xABCu64);
121        let expected_shifted = expected << U256::from(32u64);
122        let bribe = U256::from(500u64) << U256::from(8u64);
123        let recip = U256::from(3u64) << U256::from(24u64);
124        let mode = U256::from(2u64);
125        let want = expected_shifted | recip | bribe | mode;
126        assert_eq!(pack_config(2, expected, 500, 3).unwrap(), want);
127    }
128
129    #[test]
130    fn reject_check_mode_out_of_range() {
131        assert_eq!(pack_config(3, U256::ZERO, 0, 0), Ok(U256::from(3u64)));
132        assert_eq!(
133            pack_config(4, U256::ZERO, 0, 0),
134            Err(ConfigError::CheckModeOutOfRange(4))
135        );
136    }
137
138    #[test]
139    fn reject_bribe_bips_out_of_range() {
140        assert_eq!(
141            pack_config(0, U256::ZERO, 10_001, 0),
142            Err(ConfigError::BribeBipsOutOfRange(10_001)),
143        );
144    }
145
146    #[test]
147    fn reject_recipient_out_of_range() {
148        // 32 is the MAX_INDEXED_ADDRESSES bound (0..=31 valid).
149        assert_eq!(
150            pack_config(0, U256::ZERO, 0, 32),
151            Err(ConfigError::BribeRecipientOutOfRange(32)),
152        );
153    }
154
155    #[test]
156    fn accept_recipient_at_upper_bound() {
157        // 31 is the last valid table index.
158        assert!(pack_config(0, U256::ZERO, 0, 31).is_ok());
159    }
160
161    #[test]
162    fn accept_bribe_at_upper_bound() {
163        assert!(pack_config(0, U256::ZERO, 10_000, 0).is_ok());
164    }
165
166    #[test]
167    fn pack_large_expected_value_no_overflow() {
168        // ~2^200 — verifies the U256 shift doesn't clobber the low fields.
169        let v = U256::from(1u64) << U256::from(200u64);
170        let packed = pack_config(1, v, 300, 2).unwrap();
171        // Low 32 bits = (2 << 24) | (300 << 8) | 1.
172        let low = (2u64 << 24) | (300u64 << 8) | 1u64;
173        assert_eq!(packed & U256::from(u64::from(u32::MAX)), U256::from(low));
174        assert_eq!(packed >> U256::from(32u64), v);
175    }
176}