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