HoodlSwapRouter
0xa92b26bdac5cb1ff4d22a8a36341d9011b5cdc03
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
3,190 bytes
ABI entries
15
3 read · 3 write
License
none
Contract information
- Address
- 0xa92b26bdac5cb1ff4d22a8a36341d9011b5cdc03
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0x0Dc1Dd32B1…A5c0c14550
- Creation tx
- 0x26a2b117b1…1612d73f57
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (3)
hoodl() → address
key() → address, address, uint24, int24, address
poolManager() → address
Events (2)
BoughtSold
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "poolManager_",
"type": "address"
},
{
"internalType": "address",
"name": "hoodl_",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "address",
"name": "hook",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "Expired",
"type": "error"
},
{
"inputs": [],
"name": "NativeTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "OnlyPoolManager",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "out",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minOut",
"type": "uint256"
}
],
"name": "Slippage",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAmount",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "buyer",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "nativeIn",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "hoodlOut",
"type": "uint256"
}
],
"name": "Bought",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "seller",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "hoodlIn",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "nativeOut",
"type": "uint256"
}
],
"name": "Sold",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "minHoodlOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "buy",
"outputs": [
{
"internalType": "uint256",
"name": "hoodlOut",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "hoodl",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "key",
"outputs": [
{
"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"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolManager",
"outputs": [
{
"internalType": "contract IPoolManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "hoodlIn",
"type": "uint256"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "uint256",
"name": "minNativeOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "sell",
"outputs": [
{
"internalType": "uint256",
"name": "nativeOut",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "raw",
"type": "bytes"
}
],
"name": "unlockCallback",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import { IPoolManager } from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import { IHooks } from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import { IUnlockCallback } from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import { TickMath } from "@uniswap/v4-core/src/libraries/TickMath.sol";
import { BalanceDelta, BalanceDeltaLibrary } from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import { Currency } from "@uniswap/v4-core/src/types/Currency.sol";
import { PoolKey } from "@uniswap/v4-core/src/types/PoolKey.sol";
import { SwapParams } from "@uniswap/v4-core/src/types/PoolOperation.sol";
interface IERC20Minimal {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
/// @title HoodlSwapRouter
/// @notice Minimal single-pool exact-input router for the native/HOODL Uniswap v4
/// pool. One swap per call (so the hook's sell-tax provenance attributes to the real
/// seller), with slippage (minOut) and deadline protection.
contract HoodlSwapRouter is IUnlockCallback {
using BalanceDeltaLibrary for BalanceDelta;
// forge-lint: disable-next-line(screaming-snake-case-immutable)
IPoolManager public immutable poolManager;
// forge-lint: disable-next-line(screaming-snake-case-immutable)
address public immutable hoodl;
// forge-lint: disable-next-line(screaming-snake-case-immutable)
PoolKey public key;
error Expired();
error OnlyPoolManager();
error Slippage(uint256 out, uint256 minOut);
error ZeroAmount();
error NativeTransferFailed();
event Bought(address indexed buyer, uint256 nativeIn, uint256 hoodlOut);
event Sold(address indexed seller, uint256 hoodlIn, uint256 nativeOut);
struct Callback {
bool isBuy;
address payer;
address recipient;
uint256 amountIn;
}
constructor(address poolManager_, address hoodl_, uint24 fee, int24 tickSpacing, address hook) {
poolManager = IPoolManager(poolManager_);
hoodl = hoodl_;
// native (currency0) / HOODL (currency1) — native address(0) always sorts first.
key = PoolKey({
currency0: Currency.wrap(address(0)),
currency1: Currency.wrap(hoodl_),
fee: fee,
tickSpacing: tickSpacing,
hooks: IHooks(hook)
});
}
receive() external payable { }
/// @notice Buy HOODL with native ETH (exact input = msg.value).
/// @param minHoodlOut Minimum HOODL to receive, or revert (slippage guard).
function buy(address recipient, uint256 minHoodlOut, uint256 deadline)
external
payable
returns (uint256 hoodlOut)
{
if (block.timestamp > deadline) revert Expired();
if (msg.value == 0) revert ZeroAmount();
bytes memory res = poolManager.unlock(
abi.encode(
Callback({
isBuy: true,
payer: msg.sender,
recipient: recipient,
amountIn: msg.value
})
)
);
hoodlOut = abi.decode(res, (uint256));
if (hoodlOut < minHoodlOut) revert Slippage(hoodlOut, minHoodlOut);
emit Bought(msg.sender, msg.value, hoodlOut);
}
/// @notice Sell HOODL for native ETH (exact input). Approve this router for
/// `hoodlIn` first. `minNativeOut` is what lands after the hook's sell tax;
/// any decayed-rate refund is credited to the seller separately by the game.
function sell(uint256 hoodlIn, address recipient, uint256 minNativeOut, uint256 deadline)
external
returns (uint256 nativeOut)
{
if (block.timestamp > deadline) revert Expired();
if (hoodlIn == 0) revert ZeroAmount();
// Pull the seller's HOODL up front; the swap settles it from this router,
// and the game's custody provenance traces the sell back to msg.sender.
if (!IERC20Minimal(hoodl).transferFrom(msg.sender, address(this), hoodlIn)) {
revert NativeTransferFailed();
}
bytes memory res = poolManager.unlock(
abi.encode(
Callback({
isBuy: false,
payer: msg.sender,
recipient: recipient,
amountIn: hoodlIn
})
)
);
nativeOut = abi.decode(res, (uint256));
if (nativeOut < minNativeOut) revert Slippage(nativeOut, minNativeOut);
emit Sold(msg.sender, hoodlIn, nativeOut);
}
function unlockCallback(bytes calldata raw) external returns (bytes memory) {
if (msg.sender != address(poolManager)) revert OnlyPoolManager();
Callback memory cb = abi.decode(raw, (Callback));
BalanceDelta delta = poolManager.swap(
key,
SwapParams({
zeroForOne: cb.isBuy,
amountSpecified: -int256(cb.amountIn),
sqrtPriceLimitX96: cb.isBuy
? TickMath.MIN_SQRT_PRICE + 1
: TickMath.MAX_SQRT_PRICE - 1
}),
""
);
int128 nativeDelta = delta.amount0();
int128 hoodlDelta = delta.amount1();
if (cb.isBuy) {
// owe native (settle from msg.value the router holds), receive HOODL
if (nativeDelta < 0) {
poolManager.settle{ value: uint256(uint128(-nativeDelta)) }();
}
uint256 out = hoodlDelta > 0 ? uint256(uint128(hoodlDelta)) : 0;
if (out > 0) poolManager.take(key.currency1, cb.recipient, out);
return abi.encode(out);
}
// sell: owe HOODL (settle from this router's balance), receive native
if (hoodlDelta < 0) {
uint256 owed = uint256(uint128(-hoodlDelta));
poolManager.sync(key.currency1);
IERC20Minimal(hoodl).transfer(address(poolManager), owed);
poolManager.settle();
}
uint256 nativeOut = nativeDelta > 0 ? uint256(uint128(nativeDelta)) : 0;
if (nativeOut > 0) poolManager.take(key.currency0, cb.recipient, nativeOut);
return abi.encode(nativeOut);
}
}
Chain explorer5031msChain node87ms