HoodboundHook
0xf9b4b0f5d116ea0d6de6877d9e2d1f93837020cc
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
7,957 bytes
ABI entries
50
14 read · 19 write
License
none
Contract information
- Address
- 0xf9b4b0f5d116ea0d6de6877d9e2d1f93837020cc
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x4e59b44847…26c0B4956C
- Creation tx
- 0x8742bf89f2…39e2db32b1
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (14)
BPS_DENOMINATOR() → uint256
MAX_TITHE_BPS() → uint256
buyTitheBps() → uint16
gatesOpen() → bool
getHookPermissions() → tuple
maxBuyAmount() → uint256
onERC721Received(address, address, uint256, bytes) → bytes4
owner() → address
poolManager() → address
positionManager() → address
sellTitheBps() → uint16
token() → address
treasury() → address
uncollectedTithes() → uint256
Events (6)
DrawbridgeRaisedGatesThrownOpenOwnershipTransferredTitheLoweredTithesCollectedTreasurerAppointed
ABI
[
{
"inputs": [
{
"internalType": "contract IPoolManager",
"name": "poolManager_",
"type": "address"
},
{
"internalType": "contract IPositionManager",
"name": "positionManager_",
"type": "address"
},
{
"internalType": "address",
"name": "token_",
"type": "address"
},
{
"internalType": "address",
"name": "treasury_",
"type": "address"
},
{
"internalType": "uint16",
"name": "buyTitheBps_",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "sellTitheBps_",
"type": "uint16"
},
{
"internalType": "uint256",
"name": "maxBuyAmount_",
"type": "uint256"
},
{
"internalType": "address",
"name": "owner_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "HookNotImplemented",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "maxBuy",
"type": "uint256"
}
],
"name": "MaxBuyExceeded",
"type": "error"
},
{
"inputs": [],
"name": "NotPoolManager",
"type": "error"
},
{
"inputs": [],
"name": "NotTheLord",
"type": "error"
},
{
"inputs": [],
"name": "OnlyEthTokenPool",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "OwnableInvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "OwnableUnauthorizedAccount",
"type": "error"
},
{
"inputs": [],
"name": "TitheCanOnlyDecrease",
"type": "error"
},
{
"inputs": [],
"name": "TitheTooHigh",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint128",
"name": "liquidityRemoved",
"type": "uint128"
},
{
"indexed": false,
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "DrawbridgeRaised",
"type": "event"
},
{
"anonymous": false,
"inputs": [],
"name": "GatesThrownOpen",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint16",
"name": "buyTitheBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint16",
"name": "sellTitheBps",
"type": "uint16"
}
],
"name": "TitheLowered",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "address",
"name": "treasury",
"type": "address"
}
],
"name": "TithesCollected",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "treasury",
"type": "address"
}
],
"name": "TreasurerAppointed",
"type": "event"
},
{
"inputs": [],
"name": "BPS_DENOMINATOR",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_TITHE_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "abolishTithes",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"components": [
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "int256",
"name": "liquidityDelta",
"type": "int256"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"internalType": "struct ModifyLiquidityParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "BalanceDelta",
"name": "delta0",
"type": "int256"
},
{
"internalType": "BalanceDelta",
"name": "delta1",
"type": "int256"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "afterAddLiquidity",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
},
{
"internalType": "BalanceDelta",
"name": "",
"type": "int256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "afterDonate",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"internalType": "uint160",
"name": "sqrtPriceX96",
"type": "uint160"
},
{
"internalType": "int24",
"name": "tick",
"type": "int24"
}
],
"name": "afterInitialize",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"components": [
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "int256",
"name": "liquidityDelta",
"type": "int256"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"internalType": "struct ModifyLiquidityParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "BalanceDelta",
"name": "delta0",
"type": "int256"
},
{
"internalType": "BalanceDelta",
"name": "delta1",
"type": "int256"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "afterRemoveLiquidity",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
},
{
"internalType": "BalanceDelta",
"name": "",
"type": "int256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"components": [
{
"internalType": "bool",
"name": "zeroForOne",
"type": "bool"
},
{
"internalType": "int256",
"name": "amountSpecified",
"type": "int256"
},
{
"internalType": "uint160",
"name": "sqrtPriceLimitX96",
"type": "uint160"
}
],
"internalType": "struct SwapParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "BalanceDelta",
"name": "delta",
"type": "int256"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "afterSwap",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
},
{
"internalType": "int128",
"name": "",
"type": "int128"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newTreasury",
"type": "address"
}
],
"name": "appointTreasurer",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"components": [
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "int256",
"name": "liquidityDelta",
"type": "int256"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"internalType": "struct ModifyLiquidityParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "beforeAddLiquidity",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "beforeDonate",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"internalType": "uint160",
"name": "sqrtPriceX96",
"type": "uint160"
}
],
"name": "beforeInitialize",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"components": [
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "int256",
"name": "liquidityDelta",
"type": "int256"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
}
],
"internalType": "struct ModifyLiquidityParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "beforeRemoveLiquidity",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"components": [
{
"internalType": "Currency",
"name": "currency0",
"type": "address"
},
{
"internalType": "Currency",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "contract IHooks",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"components": [
{
"internalType": "bool",
"name": "zeroForOne",
"type": "bool"
},
{
"internalType": "int256",
"name": "amountSpecified",
"type": "int256"
},
{
"internalType": "uint160",
"name": "sqrtPriceLimitX96",
"type": "uint160"
}
],
"internalType": "struct SwapParams",
"name": "params",
"type": "tuple"
},
{
"internalType": "bytes",
"name": "hookData",
"type": "bytes"
}
],
"name": "beforeSwap",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
},
{
"internalType": "BeforeSwapDelta",
"name": "",
"type": "int256"
},
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "buyTitheBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "collectTheTithes",
"outputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "gatesOpen",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getHookPermissions",
"outputs": [
{
"components": [
{
"internalType": "bool",
"name": "beforeInitialize",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterInitialize",
"type": "bool"
},
{
"internalType": "bool",
"name": "beforeAddLiquidity",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterAddLiquidity",
"type": "bool"
},
{
"internalType": "bool",
"name": "beforeRemoveLiquidity",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterRemoveLiquidity",
"type": "bool"
},
{
"internalType": "bool",
"name": "beforeSwap",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterSwap",
"type": "bool"
},
{
"internalType": "bool",
"name": "beforeDonate",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterDonate",
"type": "bool"
},
{
"internalType": "bool",
"name": "beforeSwapReturnDelta",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterSwapReturnDelta",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterAddLiquidityReturnDelta",
"type": "bool"
},
{
"internalType": "bool",
"name": "afterRemoveLiquidityReturnDelta",
"type": "bool"
}
],
"internalType": "struct Hooks.Permissions",
"name": "",
"type": "tuple"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint16",
"name": "newBuyTitheBps",
"type": "uint16"
},
{
"internalType": "uint16",
"name": "newSellTitheBps",
"type": "uint16"
}
],
"name": "lowerTheTithe",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "maxBuyAmount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"name": "onERC721Received",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolManager",
"outputs": [
{
"internalType": "contract IPoolManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "positionManager",
"outputs": [
{
"internalType": "contract IPositionManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "uint128",
"name": "liquidityToRemove",
"type": "uint128"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "raiseTheDrawbridge",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "sellTitheBps",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "throwOpenTheGates",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "token",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "treasury",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "uncollectedTithes",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"name": "unlockCallback",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {BaseHook} from "uniswap-hooks/base/BaseHook.sol";
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary, toBeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol";
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
/*//////////////////////////////////////////////////////////////////////////
██╗ ██╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗███╗ ██╗██████╗
██║ ██║██╔═══██╗██╔═══██╗██╔══██╗██╔══██╗██╔═══██╗██║ ██║████╗ ██║██╔══██╗
███████║██║ ██║██║ ██║██║ ██║██████╔╝██║ ██║██║ ██║██╔██╗ ██║██║ ██║
██╔══██║██║ ██║██║ ██║██║ ██║██╔══██╗██║ ██║██║ ██║██║╚██╗██║██║ ██║
██║ ██║╚██████╔╝╚██████╔╝██████╔╝██████╔╝╚██████╔╝╚██████╔╝██║ ╚████║██████╔╝
╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚═════╝
A P2E Roguelike on Robinhood Chain
Website: https://hoodbound.io/
X: https://x.com/PlayHoodbound
//////////////////////////////////////////////////////////////////////////*/
/// @title Hoodbound Castle Gate — Uniswap v4 launch hook
/// @custom:website https://hoodbound.io/
/// @custom:x https://x.com/PlayHoodbound
/// @notice Guards the ETH/HB pool with Hoodbound's launch mechanics:
/// - 10% buy tithe / 40% sell tithe at launch; the lord may only LOWER or
/// abolish them, never raise them
/// - 1% of supply max buy per swap, until the gates are thrown open
/// - tithes are charged at swap time, ALWAYS in native ETH (buys are skimmed
/// on their ETH input, sells on their ETH output); they accrue as ERC-6909
/// claims and anyone may deliver them to the treasury via collectTheTithes()
/// - the castle (this hook) holds the LP position; `raiseTheDrawbridge`
/// lets the lord withdraw liquidity in an emergency
/// @dev Uniswap v4 does not support fee-on-transfer tokens, so the tithe lives here
/// (via afterSwap + AFTER_SWAP_RETURNS_DELTA) instead of in the token contract.
/// The pool must be native-ETH / token, which makes currency0 = ETH, currency1 = token,
/// so zeroForOne == buy.
contract HoodboundHook is BaseHook, Ownable {
using SafeCast for uint256;
uint256 public constant BPS_DENOMINATOR = 10_000;
/// @dev Hard ceiling on either tithe, protects against fat-fingered constructor args.
uint256 public constant MAX_TITHE_BPS = 5_000;
/// @notice The Uniswap v4 PositionManager, custodian of the LP position NFT.
IPositionManager public immutable positionManager;
/// @notice The token this hook serves; the only pool it can be attached to is ETH/token.
address public immutable token;
/// @notice Max tokens a single buy may receive while the gates are closed (1% of supply).
uint256 public immutable maxBuyAmount;
uint16 public buyTitheBps;
uint16 public sellTitheBps;
bool public gatesOpen;
address public treasury;
event TitheLowered(uint16 buyTitheBps, uint16 sellTitheBps);
event TithesCollected(uint256 amount, address treasury);
event GatesThrownOpen();
event TreasurerAppointed(address treasury);
event DrawbridgeRaised(uint256 tokenId, uint128 liquidityRemoved, address recipient);
error OnlyEthTokenPool();
error NotTheLord();
error MaxBuyExceeded(uint256 amount, uint256 maxBuy);
error TitheCanOnlyDecrease();
error TitheTooHigh();
error ZeroAddress();
constructor(
IPoolManager poolManager_,
IPositionManager positionManager_,
address token_,
address treasury_,
uint16 buyTitheBps_,
uint16 sellTitheBps_,
uint256 maxBuyAmount_,
address owner_
) BaseHook(poolManager_) Ownable(owner_) {
if (address(positionManager_) == address(0) || token_ == address(0) || treasury_ == address(0)) {
revert ZeroAddress();
}
if (buyTitheBps_ > MAX_TITHE_BPS || sellTitheBps_ > MAX_TITHE_BPS) revert TitheTooHigh();
positionManager = positionManager_;
token = token_;
treasury = treasury_;
buyTitheBps = buyTitheBps_;
sellTitheBps = sellTitheBps_;
maxBuyAmount = maxBuyAmount_;
}
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: true,
afterInitialize: false,
beforeAddLiquidity: false,
afterAddLiquidity: false,
beforeRemoveLiquidity: false,
afterRemoveLiquidity: false,
beforeSwap: true,
afterSwap: true,
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: true,
afterSwapReturnDelta: true,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
/// @dev Only allow this hook to be attached to the native-ETH / token pool, so the
/// buy/sell direction assumptions below always hold. Additionally, only the
/// owner may create the pool: the contracts are deployed before launch, and
/// without this check anyone could pre-initialize the pool at a hostile price.
function _beforeInitialize(address, PoolKey calldata key, uint160) internal view override returns (bytes4) {
if (!key.currency0.isAddressZero() || Currency.unwrap(key.currency1) != token) revert OnlyEthTokenPool();
if (tx.origin != owner()) revert NotTheLord();
return this.beforeInitialize.selector;
}
/// @dev The tithe is ALWAYS charged in native ETH (currency0). When ETH is the
/// swap's "specified" currency (exact-input buys, exact-output sells) it must
/// be skimmed here in beforeSwap; the remaining cases are handled in afterSwap.
function _beforeSwap(address, PoolKey calldata, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
bool isBuy = params.zeroForOne;
uint256 titheBps = isBuy ? buyTitheBps : sellTitheBps;
// ETH (currency0) is specified on exact-input buys and exact-output sells
bool ethIsSpecified = isBuy == (params.amountSpecified < 0);
if (titheBps == 0 || !ethIsSpecified) {
return (this.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}
uint256 absSpecified =
params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);
uint256 feeAmount = absSpecified * titheBps / BPS_DENOMINATOR;
if (feeAmount == 0) return (this.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
// buys: skim the ETH input before it hits the pool
// exact-output sells: gross the swap up so the user still receives the full
// requested ETH, with the tithe taken on top
_accrueTithe(feeAmount);
return (this.beforeSwap.selector, toBeforeSwapDelta(feeAmount.toInt128(), 0), 0);
}
function _afterSwap(address, PoolKey calldata, SwapParams calldata params, BalanceDelta delta, bytes calldata)
internal
override
returns (bytes4, int128)
{
// currency0 = native ETH, currency1 = token (enforced in _beforeInitialize)
bool isBuy = params.zeroForOne;
if (isBuy && !gatesOpen) {
// delta.amount1() is positive on a buy: tokens credited to the swapper
uint256 tokensOut = uint256(uint128(delta.amount1()));
if (tokensOut > maxBuyAmount) revert MaxBuyExceeded(tokensOut, maxBuyAmount);
}
uint256 titheBps = isBuy ? buyTitheBps : sellTitheBps;
if (titheBps == 0) return (this.afterSwap.selector, 0);
// ETH is the "unspecified" currency on exact-output buys and exact-input sells;
// the other two cases were already tithed in _beforeSwap.
bool ethIsUnspecified = (params.amountSpecified < 0) != isBuy;
if (!ethIsUnspecified) return (this.afterSwap.selector, 0);
int128 ethAmount = delta.amount0();
uint256 absEth = ethAmount < 0 ? uint256(uint128(-ethAmount)) : uint256(uint128(ethAmount));
uint256 feeAmount = absEth * titheBps / BPS_DENOMINATOR;
if (feeAmount == 0) return (this.afterSwap.selector, 0);
_accrueTithe(feeAmount);
return (this.afterSwap.selector, feeAmount.toInt128());
}
/// @dev The swapper's ETH only physically reaches the PoolManager when the router
/// settles at the end of the transaction, so the tithe cannot be transferred
/// out mid-swap. Instead it accrues as an ERC-6909 claim (pure accounting,
/// backed by the swap's settlement) and is redeemed via collectTheTithes().
function _accrueTithe(uint256 feeAmount) internal {
poolManager.mint(address(this), 0, feeAmount); // id 0 = native ETH
}
/// @notice Redeem all accrued tithes as native ETH and deliver them to the treasury.
/// @dev Permissionless on purpose: the ETH can only ever go to the treasury.
function collectTheTithes() external returns (uint256 amount) {
amount = abi.decode(poolManager.unlock(""), (uint256));
}
/// @notice ETH tithes accrued and not yet collected.
function uncollectedTithes() external view returns (uint256) {
return poolManager.balanceOf(address(this), 0);
}
function unlockCallback(bytes calldata) external onlyPoolManager returns (bytes memory) {
uint256 amount = poolManager.balanceOf(address(this), 0);
if (amount > 0) {
poolManager.burn(address(this), 0, amount);
poolManager.take(Currency.wrap(address(0)), treasury, amount);
emit TithesCollected(amount, treasury);
}
return abi.encode(amount);
}
// ------------------------------------------------------------------
// The lord's decrees (owner controls)
// ------------------------------------------------------------------
/// @notice Lower the tithes. Tithes can never be raised again once lowered.
function lowerTheTithe(uint16 newBuyTitheBps, uint16 newSellTitheBps) public onlyOwner {
if (newBuyTitheBps > buyTitheBps || newSellTitheBps > sellTitheBps) revert TitheCanOnlyDecrease();
buyTitheBps = newBuyTitheBps;
sellTitheBps = newSellTitheBps;
emit TitheLowered(newBuyTitheBps, newSellTitheBps);
}
/// @notice Abolish both tithes permanently (they can never be re-imposed).
function abolishTithes() external onlyOwner {
lowerTheTithe(0, 0);
}
/// @notice Throw open the gates: remove the max-buy limit permanently.
function throwOpenTheGates() external onlyOwner {
gatesOpen = true;
emit GatesThrownOpen();
}
/// @notice Appoint a new treasurer to collect the tithes. Must accept native ETH.
function appointTreasurer(address newTreasury) external onlyOwner {
if (newTreasury == address(0)) revert ZeroAddress();
treasury = newTreasury;
emit TreasurerAppointed(newTreasury);
}
/// @notice Emergency withdrawal of pool liquidity held by this hook.
/// @dev Decreases the LP position (which this contract owns as an NFT) via the
/// PositionManager and sends the resulting ETH + tokens to `recipient`.
/// @param tokenId The LP position NFT id (logged at deployment).
/// @param liquidityToRemove Liquidity units to withdraw; pass 0 to withdraw everything.
/// @param recipient Where the withdrawn ETH and tokens go. Must accept native ETH.
function raiseTheDrawbridge(uint256 tokenId, uint128 liquidityToRemove, address recipient) external onlyOwner {
if (recipient == address(0)) revert ZeroAddress();
uint128 positionLiquidity = positionManager.getPositionLiquidity(tokenId);
uint128 toRemove = (liquidityToRemove == 0 || liquidityToRemove > positionLiquidity)
? positionLiquidity
: liquidityToRemove;
bytes memory actions = abi.encodePacked(uint8(Actions.DECREASE_LIQUIDITY), uint8(Actions.TAKE_PAIR));
bytes[] memory params = new bytes[](2);
params[0] = abi.encode(tokenId, uint256(toRemove), uint128(0), uint128(0), bytes(""));
params[1] = abi.encode(Currency.wrap(address(0)), Currency.wrap(token), recipient);
positionManager.modifyLiquidities(abi.encode(actions, params), block.timestamp);
emit DrawbridgeRaised(tokenId, toRemove, recipient);
}
/// @dev Lets the PositionManager mint the LP position NFT directly to this contract.
function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
return this.onERC721Received.selector;
}
}
Chain explorer3054msChain node82ms