use alloy_primitives::Address;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum HookFlag {
AfterRemoveLiquidityReturnsDelta = 0,
AfterAddLiquidityReturnsDelta = 1,
AfterSwapReturnsDelta = 2,
BeforeSwapReturnsDelta = 3,
AfterDonate = 4,
BeforeDonate = 5,
AfterSwap = 6,
BeforeSwap = 7,
AfterRemoveLiquidity = 8,
BeforeRemoveLiquidity = 9,
AfterAddLiquidity = 10,
BeforeAddLiquidity = 11,
AfterInitialize = 12,
BeforeInitialize = 13,
}
pub(crate) const SWAP_PERMISSIONS_MASK: u16 = (1 << HookFlag::BeforeSwap as u16)
| (1 << HookFlag::AfterSwap as u16)
| (1 << HookFlag::BeforeSwapReturnsDelta as u16)
| (1 << HookFlag::AfterSwapReturnsDelta as u16);
#[inline]
#[must_use]
pub const fn has_permission(hooks: Address, flag: HookFlag) -> bool {
let bytes = hooks.0 .0;
let mask = ((bytes[18] as u16) << 8) | (bytes[19] as u16);
let bit = flag as u16;
mask & (1 << bit) != 0
}
#[inline]
#[must_use]
pub const fn has_swap_permissions(hooks: Address) -> bool {
let bytes = hooks.0 .0;
let mask = ((bytes[18] as u16) << 8) | (bytes[19] as u16);
mask & SWAP_PERMISSIONS_MASK != 0
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::address;
fn hook_address_with(flags: &[HookFlag]) -> Address {
let mut bytes = [0u8; 20];
let mut mask: u16 = 0;
for f in flags {
mask |= 1 << (*f as u16);
}
bytes[18] = (mask >> 8) as u8;
bytes[19] = mask as u8;
Address::from(bytes)
}
#[test]
fn flag_bit_indices_match_v4_hooks_sol() {
assert_eq!(HookFlag::AfterRemoveLiquidityReturnsDelta as u8, 0);
assert_eq!(HookFlag::AfterAddLiquidityReturnsDelta as u8, 1);
assert_eq!(HookFlag::AfterSwapReturnsDelta as u8, 2);
assert_eq!(HookFlag::BeforeSwapReturnsDelta as u8, 3);
assert_eq!(HookFlag::AfterDonate as u8, 4);
assert_eq!(HookFlag::BeforeDonate as u8, 5);
assert_eq!(HookFlag::AfterSwap as u8, 6);
assert_eq!(HookFlag::BeforeSwap as u8, 7);
assert_eq!(HookFlag::AfterRemoveLiquidity as u8, 8);
assert_eq!(HookFlag::BeforeRemoveLiquidity as u8, 9);
assert_eq!(HookFlag::AfterAddLiquidity as u8, 10);
assert_eq!(HookFlag::BeforeAddLiquidity as u8, 11);
assert_eq!(HookFlag::AfterInitialize as u8, 12);
assert_eq!(HookFlag::BeforeInitialize as u8, 13);
}
#[test]
fn swap_permissions_mask_is_0xcc() {
assert_eq!(SWAP_PERMISSIONS_MASK, 0x00CC);
assert_eq!(SWAP_PERMISSIONS_MASK, 0b1100_1100);
}
#[test]
fn each_flag_decodes_in_isolation() {
let all = [
HookFlag::AfterRemoveLiquidityReturnsDelta,
HookFlag::AfterAddLiquidityReturnsDelta,
HookFlag::AfterSwapReturnsDelta,
HookFlag::BeforeSwapReturnsDelta,
HookFlag::AfterDonate,
HookFlag::BeforeDonate,
HookFlag::AfterSwap,
HookFlag::BeforeSwap,
HookFlag::AfterRemoveLiquidity,
HookFlag::BeforeRemoveLiquidity,
HookFlag::AfterAddLiquidity,
HookFlag::BeforeAddLiquidity,
HookFlag::AfterInitialize,
HookFlag::BeforeInitialize,
];
for set_flag in all {
let addr = hook_address_with(&[set_flag]);
for probe in all {
let expected = probe == set_flag;
assert_eq!(
has_permission(addr, probe),
expected,
"addr={addr:?} set={set_flag:?} probe={probe:?}",
);
}
}
}
#[test]
fn zero_address_has_no_permissions() {
assert!(!has_swap_permissions(Address::ZERO));
for f in [
HookFlag::BeforeSwap,
HookFlag::AfterSwap,
HookFlag::BeforeSwapReturnsDelta,
HookFlag::AfterSwapReturnsDelta,
HookFlag::BeforeInitialize,
HookFlag::AfterRemoveLiquidityReturnsDelta,
] {
assert!(!has_permission(Address::ZERO, f));
}
}
#[test]
fn all_ones_address_has_every_permission() {
let addr = Address::from([0xFFu8; 20]);
let all = [
HookFlag::AfterRemoveLiquidityReturnsDelta,
HookFlag::AfterAddLiquidityReturnsDelta,
HookFlag::AfterSwapReturnsDelta,
HookFlag::BeforeSwapReturnsDelta,
HookFlag::AfterDonate,
HookFlag::BeforeDonate,
HookFlag::AfterSwap,
HookFlag::BeforeSwap,
HookFlag::AfterRemoveLiquidity,
HookFlag::BeforeRemoveLiquidity,
HookFlag::AfterAddLiquidity,
HookFlag::BeforeAddLiquidity,
HookFlag::AfterInitialize,
HookFlag::BeforeInitialize,
];
for f in all {
assert!(has_permission(addr, f));
}
assert!(has_swap_permissions(addr));
}
#[test]
fn swap_permissions_trips_on_each_swap_flag_independently() {
for flag in [
HookFlag::BeforeSwap,
HookFlag::AfterSwap,
HookFlag::BeforeSwapReturnsDelta,
HookFlag::AfterSwapReturnsDelta,
] {
let addr = hook_address_with(&[flag]);
assert!(has_swap_permissions(addr), "swap flag {flag:?} must trip the gate");
}
}
#[test]
fn swap_permissions_ignores_non_swap_flags() {
for flag in [
HookFlag::BeforeInitialize,
HookFlag::AfterInitialize,
HookFlag::BeforeAddLiquidity,
HookFlag::AfterAddLiquidity,
HookFlag::BeforeRemoveLiquidity,
HookFlag::AfterRemoveLiquidity,
HookFlag::BeforeDonate,
HookFlag::AfterDonate,
HookFlag::AfterAddLiquidityReturnsDelta,
HookFlag::AfterRemoveLiquidityReturnsDelta,
] {
let addr = hook_address_with(&[flag]);
assert!(!has_swap_permissions(addr), "non-swap flag {flag:?} must not trip the gate");
}
}
#[test]
fn high_address_bits_dont_affect_permissions() {
let addr = address!("dead00112233445566778899aabbccddeeff0100");
assert!(!has_swap_permissions(addr), "no swap flags in low 16 bits");
assert!(has_permission(addr, HookFlag::AfterRemoveLiquidity), "bit 8 must decode");
assert!(!has_permission(addr, HookFlag::BeforeSwap));
assert!(!has_permission(addr, HookFlag::BeforeInitialize));
}
#[test]
fn permission_byte_offset_is_18_and_19() {
let before_swap = address!("0000000000000000000000000000000000000080");
assert!(has_permission(before_swap, HookFlag::BeforeSwap));
assert!(has_swap_permissions(before_swap));
assert!(!has_permission(before_swap, HookFlag::AfterSwap));
let before_init = address!("0000000000000000000000000000000000002000");
assert!(has_permission(before_init, HookFlag::BeforeInitialize));
assert!(!has_swap_permissions(before_init));
let arm_lrd = address!("0000000000000000000000000000000000000001");
assert!(has_permission(arm_lrd, HookFlag::AfterRemoveLiquidityReturnsDelta));
assert!(!has_swap_permissions(arm_lrd));
let off_by_one = address!("0000000000000000000000000000000000800000");
assert!(!has_swap_permissions(off_by_one), "byte 17 must not affect decoding");
for f in [
HookFlag::BeforeSwap,
HookFlag::AfterSwap,
HookFlag::BeforeSwapReturnsDelta,
HookFlag::AfterSwapReturnsDelta,
HookFlag::BeforeInitialize,
] {
assert!(!has_permission(off_by_one, f), "byte 17 leaked into flag {f:?}");
}
}
#[test]
fn real_v4_mainnet_hooks_decode_correctly() {
let hook_a = address!("0d62529346ac2c61f5c0582210d01214687bc0cc");
assert!(has_swap_permissions(hook_a), "hook_a must have swap permissions");
assert!(has_permission(hook_a, HookFlag::BeforeSwap));
assert!(has_permission(hook_a, HookFlag::AfterSwap));
assert!(has_permission(hook_a, HookFlag::BeforeSwapReturnsDelta));
assert!(has_permission(hook_a, HookFlag::AfterSwapReturnsDelta));
assert!(!has_permission(hook_a, HookFlag::BeforeInitialize));
assert!(!has_permission(hook_a, HookFlag::AfterInitialize));
assert!(!has_permission(hook_a, HookFlag::BeforeAddLiquidity));
let hook_b = address!("627fa6f76fa96b10bae1b6fba280a3c9264500cc");
assert!(has_swap_permissions(hook_b));
assert!(has_permission(hook_b, HookFlag::BeforeSwap));
assert!(has_permission(hook_b, HookFlag::AfterSwap));
assert!(has_permission(hook_b, HookFlag::BeforeSwapReturnsDelta));
assert!(has_permission(hook_b, HookFlag::AfterSwapReturnsDelta));
let hook_c = address!("a2dcd7bf7ff3c014a855bf00799ccf07e6c800cc");
assert!(has_swap_permissions(hook_c));
assert!(has_permission(hook_c, HookFlag::AfterSwap));
let bytes_a = hook_a.0 .0;
let mask_a = ((bytes_a[18] as u16) << 8) | (bytes_a[19] as u16);
assert_eq!(mask_a, 0xc0cc, "hook_a low-16 mask preserved as captured");
let bytes_b = hook_b.0 .0;
let mask_b = ((bytes_b[18] as u16) << 8) | (bytes_b[19] as u16);
assert_eq!(mask_b, 0x00cc, "hook_b is the canonical 0xCC swap-only mask");
}
}