AlpsRouter
0xee147478b8910f426fdd5ce763f75ee9f3dd842e
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
11,833 bytes
ABI entries
56
19 read · 10 write
License
none
Contract information
- Address
- 0xee147478b8910f426fdd5ce763f75ee9f3dd842e
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x5EcC02c691…75386ad57d
- Creation tx
- 0xdd3323a064…073bd2ba35
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (19)
BPS() → uint256
MAX_RETRO_FEE() → uint256
MAX_SLIPPAGE_BPS() → uint256
OBS_CARDINALITY() → uint16
ORDER_TTL() → uint64
PLATFORM_FEE_BPS() → uint256
TWAP_MAX() → uint32
TWAP_MIN() → uint32
factory() → address
feeSink() → address
keeper() → address
mintedBy(uint256) → address
npm() → address
orders(uint256) → int24, int24, uint16, uint64, address
owner() → address
pendingOwner() → address
retroEnableFeeWei() → uint256
swapRouter() → address
weth() → address
Events (9)
KeeperChangedOrderCancelledOrderExecutedOrderRegisteredOwnershipTransferStartedOwnershipTransferredRetroEnableFeeChangedRetroEnabledZapMinted
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "npm_",
"type": "address"
},
{
"internalType": "address",
"name": "swapRouter_",
"type": "address"
},
{
"internalType": "address",
"name": "factory_",
"type": "address"
},
{
"internalType": "address",
"name": "weth_",
"type": "address"
},
{
"internalType": "address",
"name": "feeSink_",
"type": "address"
},
{
"internalType": "address",
"name": "keeper_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "BadParams",
"type": "error"
},
{
"inputs": [],
"name": "FeeTooHigh",
"type": "error"
},
{
"inputs": [],
"name": "NativeMismatch",
"type": "error"
},
{
"inputs": [],
"name": "NoOrder",
"type": "error"
},
{
"inputs": [],
"name": "NotApproved",
"type": "error"
},
{
"inputs": [],
"name": "NotKeeper",
"type": "error"
},
{
"inputs": [],
"name": "NotPositionOwner",
"type": "error"
},
{
"inputs": [],
"name": "NotPremium",
"type": "error"
},
{
"inputs": [],
"name": "NotTriggered",
"type": "error"
},
{
"inputs": [],
"name": "OrderExpired",
"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": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "StaleOracle",
"type": "error"
},
{
"inputs": [],
"name": "T",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "keeper",
"type": "address"
}
],
"name": "KeeperChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "OrderCancelled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": false,
"internalType": "int24",
"name": "spotTick",
"type": "int24"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount1",
"type": "uint256"
}
],
"name": "OrderExecuted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "int24",
"name": "tickBelow",
"type": "int24"
},
{
"indexed": false,
"internalType": "int24",
"name": "tickAbove",
"type": "int24"
},
{
"indexed": false,
"internalType": "uint16",
"name": "slippageBps",
"type": "uint16"
},
{
"indexed": false,
"internalType": "uint64",
"name": "expiry",
"type": "uint64"
}
],
"name": "OrderRegistered",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferStarted",
"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": "uint256",
"name": "feeWei",
"type": "uint256"
}
],
"name": "RetroEnableFeeChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "user",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "feePaid",
"type": "uint256"
}
],
"name": "RetroEnabled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "user",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "address",
"name": "tokenIn",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "feeTaken",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
}
],
"name": "ZapMinted",
"type": "event"
},
{
"inputs": [],
"name": "BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_RETRO_FEE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_SLIPPAGE_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "OBS_CARDINALITY",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "ORDER_TTL",
"outputs": [
{
"internalType": "uint64",
"name": "",
"type": "uint64"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "PLATFORM_FEE_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TWAP_MAX",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TWAP_MIN",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "cancelOrder",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "enableExisting",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "executeOrder",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "factory",
"outputs": [
{
"internalType": "contract IUniswapV3Factory",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "feeSink",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "keeper",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "mintedBy",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "npm",
"outputs": [
{
"internalType": "contract INonfungiblePositionManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "orders",
"outputs": [
{
"internalType": "int24",
"name": "tickBelow",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickAbove",
"type": "int24"
},
{
"internalType": "uint16",
"name": "slippageBps",
"type": "uint16"
},
{
"internalType": "uint64",
"name": "expiry",
"type": "uint64"
},
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "int24",
"name": "tickBelow",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickAbove",
"type": "int24"
},
{
"internalType": "uint16",
"name": "slippageBps",
"type": "uint16"
}
],
"name": "registerOrder",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "retroEnableFeeWei",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "keeper_",
"type": "address"
}
],
"name": "setKeeper",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "feeWei",
"type": "uint256"
}
],
"name": "setRetroEnableFee",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "swapRouter",
"outputs": [
{
"internalType": "contract ISwapRouter02",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "weth",
"outputs": [
{
"internalType": "contract IWETH",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "tokenIn",
"type": "address"
},
{
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"internalType": "address",
"name": "token0",
"type": "address"
},
{
"internalType": "address",
"name": "token1",
"type": "address"
},
{
"internalType": "uint24",
"name": "poolFee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "uint256",
"name": "swapAmountIn",
"type": "uint256"
},
{
"internalType": "uint24",
"name": "swapPoolFee",
"type": "uint24"
},
{
"internalType": "uint256",
"name": "swapMinOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount0Min",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1Min",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"internalType": "struct AlpsRouter.ZapParams",
"name": "p",
"type": "tuple"
}
],
"name": "zapMint",
"outputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20 as OZIERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {TickMath} from "@uniswap/v3-core/contracts/libraries/TickMath.sol";
import {LiquidityAmounts} from "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol";
import {
IERC20,
IWETH,
INonfungiblePositionManager,
ISwapRouter02,
IUniswapV3Factory,
IUniswapV3Pool
} from "./interfaces/IExternal.sol";
/// @title AlpsRouter — zap mints + TP/SL automation for Uniswap v3 on Robinhood Chain
///
/// The basic Alps terminal talks to Uniswap directly and always will. This
/// contract is the OPT-IN premium lane:
///
/// - zapMint: pull one token, take the 0.1% platform fee, optionally swap a
/// slice, mint the position — NFT goes straight to the user. Funds only
/// exist here inside the transaction; nothing rests in this contract.
/// - TP/SL: for positions minted here (or retro-enabled), the owner stores
/// trigger ticks. Our keeper may then close the position when BOTH the
/// spot tick and a 60s TWAP breach the trigger — and the proceeds can go
/// nowhere except the position owner's wallet.
///
/// Trust surface, in full:
/// - PLATFORM_FEE_BPS and feeSink are immutable. The fee can never be raised.
/// - The owner can only rotate the keeper address and set the retro-enable
/// price (hard-capped). No pause, no upgrade, no fund access.
/// - A compromised keeper can at worst trigger a close early, bounded by
/// the owner's own slippage setting and the TWAP guard.
/// - The user can revoke the NFT approval at any time and manage the
/// position on Uniswap directly. Features are gated here, funds never.
contract AlpsRouter is Ownable2Step, ReentrancyGuard {
using SafeERC20 for OZIERC20;
// ---------------------------------------------------------- constants --
uint256 public constant PLATFORM_FEE_BPS = 10; // 0.1%, immutable forever
uint256 public constant BPS = 10_000;
uint64 public constant ORDER_TTL = 30 days;
uint256 public constant MAX_RETRO_FEE = 0.01 ether;
// Robinhood Chain is a ~100ms-block Orbit chain; Uniswap's oracle still
// writes at most one observation per SECOND. A fixed 60s lookback reverts
// 'OLD' on young pools and is griefable, so the trigger TWAP is ADAPTIVE:
// it uses the largest window the pool can answer, capped at MAX and floored
// at MIN (below the floor executeOrder fails closed until history accrues).
uint32 public constant TWAP_MAX = 30; // seconds — manipulation-resistance cap
uint32 public constant TWAP_MIN = 5; // seconds — refuse to trigger on less
uint16 public constant OBS_CARDINALITY = 64; // one-time per-pool ring size
uint256 public constant MAX_SLIPPAGE_BPS = 500; // 5%
INonfungiblePositionManager public immutable npm;
ISwapRouter02 public immutable swapRouter;
IUniswapV3Factory public immutable factory;
IWETH public immutable weth;
address public immutable feeSink;
// ------------------------------------------------------------ storage --
/// premium gate: positions minted through this contract (or retro-enabled)
mapping(uint256 => address) public mintedBy;
struct Order {
// trigger when tick <= tickBelow (stop side) or tick >= tickAbove
// (target side); use sentinels to disable a side
int24 tickBelow;
int24 tickAbove;
uint16 slippageBps;
uint64 expiry;
// owner at registration — a transfer invalidates the order so the
// previous owner's triggers can never close the new owner's position
address owner;
}
mapping(uint256 => Order) public orders;
address public keeper;
uint256 public retroEnableFeeWei = 0.002 ether;
// ------------------------------------------------------------- events --
event ZapMinted(
address indexed user,
uint256 indexed tokenId,
address tokenIn,
uint256 amountIn,
uint256 feeTaken,
uint128 liquidity
);
event OrderRegistered(
uint256 indexed tokenId, int24 tickBelow, int24 tickAbove, uint16 slippageBps, uint64 expiry
);
event OrderCancelled(uint256 indexed tokenId);
event OrderExecuted(
uint256 indexed tokenId, address indexed owner, int24 spotTick, uint256 amount0, uint256 amount1
);
event RetroEnabled(uint256 indexed tokenId, address indexed user, uint256 feePaid);
event KeeperChanged(address indexed keeper);
event RetroEnableFeeChanged(uint256 feeWei);
// ------------------------------------------------------------- errors --
error NotKeeper();
error NotPositionOwner();
error NotPremium();
error NoOrder();
error OrderExpired();
error NotTriggered();
error NotApproved();
error BadParams();
error FeeTooHigh();
error NativeMismatch();
error StaleOracle();
constructor(
address npm_,
address swapRouter_,
address factory_,
address weth_,
address feeSink_,
address keeper_
) Ownable(msg.sender) {
if (
npm_ == address(0) || swapRouter_ == address(0) || factory_ == address(0)
|| weth_ == address(0) || feeSink_ == address(0)
) revert BadParams();
npm = INonfungiblePositionManager(npm_);
swapRouter = ISwapRouter02(swapRouter_);
factory = IUniswapV3Factory(factory_);
weth = IWETH(weth_);
feeSink = feeSink_;
keeper = keeper_;
}
// only WETH may send native here (unwrap during refunds)
receive() external payable {
if (msg.sender != address(weth)) revert BadParams();
}
// -------------------------------------------------------------- admin --
function setKeeper(address keeper_) external onlyOwner {
keeper = keeper_;
emit KeeperChanged(keeper_);
}
function setRetroEnableFee(uint256 feeWei) external onlyOwner {
if (feeWei > MAX_RETRO_FEE) revert FeeTooHigh();
retroEnableFeeWei = feeWei;
emit RetroEnableFeeChanged(feeWei);
}
// ---------------------------------------------------------------- zap --
struct ZapParams {
address tokenIn; // must be token0 or token1 of the target pool
uint256 amountIn; // total deposit, fee comes out of this
address token0;
address token1;
uint24 poolFee;
int24 tickLower;
int24 tickUpper;
uint256 swapAmountIn; // slice of the net deposit swapped into the other token (0 = single-sided)
uint24 swapPoolFee; // fee tier of the pool used for that swap
uint256 swapMinOut;
uint256 amount0Min; // mint slippage floors
uint256 amount1Min;
uint256 deadline;
}
/// Pull tokenIn (or native ETH when tokenIn is WETH), take the platform
/// fee, optionally swap a slice, mint to msg.sender, refund every wei of
/// leftovers. The NFT never belongs to this contract.
function zapMint(ZapParams calldata p)
external
payable
nonReentrant
returns (uint256 tokenId, uint128 liquidity)
{
if (p.tokenIn != p.token0 && p.tokenIn != p.token1) revert BadParams();
if (p.amountIn == 0) revert BadParams();
// fund: native path wraps, ERC20 path pulls
if (msg.value > 0) {
if (p.tokenIn != address(weth) || msg.value != p.amountIn) revert NativeMismatch();
weth.deposit{value: msg.value}();
} else {
OZIERC20(p.tokenIn).safeTransferFrom(msg.sender, address(this), p.amountIn);
}
// the 0.1% — the only value this contract ever keeps, and it goes
// straight out to the immutable sink
uint256 fee = (p.amountIn * PLATFORM_FEE_BPS) / BPS;
if (fee > 0) OZIERC20(p.tokenIn).safeTransfer(feeSink, fee);
uint256 net = p.amountIn - fee;
address tokenOther = p.tokenIn == p.token0 ? p.token1 : p.token0;
if (p.swapAmountIn > net) revert BadParams();
if (p.swapAmountIn > 0) {
OZIERC20(p.tokenIn).forceApprove(address(swapRouter), p.swapAmountIn);
swapRouter.exactInputSingle(
ISwapRouter02.ExactInputSingleParams({
tokenIn: p.tokenIn,
tokenOut: tokenOther,
fee: p.swapPoolFee,
recipient: address(this),
amountIn: p.swapAmountIn,
amountOutMinimum: p.swapMinOut,
sqrtPriceLimitX96: 0
})
);
}
uint256 bal0 = IERC20(p.token0).balanceOf(address(this));
uint256 bal1 = IERC20(p.token1).balanceOf(address(this));
OZIERC20(p.token0).forceApprove(address(npm), bal0);
OZIERC20(p.token1).forceApprove(address(npm), bal1);
(tokenId, liquidity,,) = npm.mint(
INonfungiblePositionManager.MintParams({
token0: p.token0,
token1: p.token1,
fee: p.poolFee,
tickLower: p.tickLower,
tickUpper: p.tickUpper,
amount0Desired: bal0,
amount1Desired: bal1,
amount0Min: p.amount0Min,
amount1Min: p.amount1Min,
recipient: msg.sender,
deadline: p.deadline
})
);
mintedBy[tokenId] = msg.sender;
// refund leftovers of both sides, unwrapping if the user paid native
_refund(p.token0, msg.sender, msg.value > 0);
_refund(p.token1, msg.sender, msg.value > 0);
emit ZapMinted(msg.sender, tokenId, p.tokenIn, p.amountIn, fee, liquidity);
}
function _refund(address token, address to, bool paidNative) internal {
uint256 bal = IERC20(token).balanceOf(address(this));
if (bal == 0) return;
if (paidNative && token == address(weth)) {
weth.withdraw(bal);
(bool ok,) = to.call{value: bal}("");
if (!ok) revert BadParams();
} else {
OZIERC20(token).safeTransfer(to, bal);
}
}
// -------------------------------------------------------- retro-enable --
/// Bring an existing Uniswap-minted position into the premium lane.
function enableExisting(uint256 tokenId) external payable nonReentrant {
if (npm.ownerOf(tokenId) != msg.sender) revert NotPositionOwner();
if (mintedBy[tokenId] != address(0)) revert BadParams();
if (msg.value < retroEnableFeeWei) revert BadParams();
(bool ok,) = feeSink.call{value: msg.value}("");
if (!ok) revert BadParams();
mintedBy[tokenId] = msg.sender;
emit RetroEnabled(tokenId, msg.sender, msg.value);
}
// -------------------------------------------------------------- orders --
function registerOrder(uint256 tokenId, int24 tickBelow, int24 tickAbove, uint16 slippageBps)
external
{
// premium is a property of the TOKEN (minted here or retro-enabled),
// so it survives transfers — any CURRENT owner of a premium position
// may arm it. mintedBy is only a flag, never a control handle: every
// path also requires current ownership.
if (npm.ownerOf(tokenId) != msg.sender) revert NotPositionOwner();
if (mintedBy[tokenId] == address(0)) revert NotPremium();
if (slippageBps > MAX_SLIPPAGE_BPS) revert BadParams();
// at least one live side, and a coherent window
bool hasBelow = tickBelow > TickMath.MIN_TICK;
bool hasAbove = tickAbove < TickMath.MAX_TICK;
if (!hasBelow && !hasAbove) revert BadParams();
if (hasBelow && hasAbove && tickBelow >= tickAbove) revert BadParams();
orders[tokenId] = Order({
tickBelow: tickBelow,
tickAbove: tickAbove,
slippageBps: slippageBps,
expiry: uint64(block.timestamp) + ORDER_TTL,
owner: msg.sender
});
// grow the pool's oracle ring so it can answer the trigger TWAP.
// No-op (and free) on pools already at/above this size, e.g. the
// busy majors. Fresh pools pay it once.
(,, address t0, address t1, uint24 fee_,,,,,,,) = npm.positions(tokenId);
IUniswapV3Pool pool = IUniswapV3Pool(factory.getPool(t0, t1, fee_));
pool.increaseObservationCardinalityNext(OBS_CARDINALITY);
emit OrderRegistered(tokenId, tickBelow, tickAbove, slippageBps, orders[tokenId].expiry);
}
function cancelOrder(uint256 tokenId) external {
if (npm.ownerOf(tokenId) != msg.sender) revert NotPositionOwner();
if (orders[tokenId].expiry == 0) revert NoOrder();
delete orders[tokenId];
emit OrderCancelled(tokenId);
}
/// Keeper-triggered close. Fails closed on every uncertainty: no order,
/// expired, no approval, trigger not breached by BOTH spot and TWAP, or
/// proceeds under the owner's slippage floor.
function executeOrder(uint256 tokenId) external nonReentrant {
if (msg.sender != keeper) revert NotKeeper();
Order memory o = orders[tokenId];
if (o.expiry == 0) revert NoOrder();
if (block.timestamp > o.expiry) revert OrderExpired();
address owner_ = npm.ownerOf(tokenId);
// a transferred position drops its old order: the trigger belongs to
// whoever set it, and must never fire for a new owner
if (owner_ != o.owner) revert NotPositionOwner();
if (!npm.isApprovedForAll(owner_, address(this)) && npm.getApproved(tokenId) != address(this))
{
revert NotApproved();
}
(,, address t0, address t1, uint24 fee_, int24 tickLower, int24 tickUpper, uint128 liq,,,,) =
npm.positions(tokenId);
if (liq == 0) revert NoOrder();
IUniswapV3Pool pool = IUniswapV3Pool(factory.getPool(t0, t1, fee_));
// trigger requires BOTH spot and an adaptive TWAP to breach — a single
// manipulated block cannot force a close
(uint160 spotSqrtP, int24 spotTick,,,,,) = pool.slot0();
int24 twapTick = _consultTwap(pool);
bool below = spotTick <= o.tickBelow && twapTick <= o.tickBelow;
bool above = spotTick >= o.tickAbove && twapTick >= o.tickAbove;
if (!below && !above) revert NotTriggered();
// slippage floor from the SPOT price where the burn actually settles —
// floors from the (divergent) TWAP made decreaseLiquidity revert during
// fast moves, the exact moment a stop must fire.
//
// INVARIANT (load-bearing, do not break in any refactor): this floor is
// safe ONLY because executeOrder performs NO swap and collect() below
// hard-routes proceeds to the position owner. A pure liquidity removal
// cannot be sandwiched for the owner's loss (LP value is convex, minimal
// at the true price). If a swap or a non-owner recipient is ever added
// here, this spot-derived floor stops protecting and must be replaced.
(uint256 exp0, uint256 exp1) = LiquidityAmounts.getAmountsForLiquidity(
spotSqrtP,
TickMath.getSqrtRatioAtTick(tickLower),
TickMath.getSqrtRatioAtTick(tickUpper),
liq
);
delete orders[tokenId];
npm.decreaseLiquidity(
INonfungiblePositionManager.DecreaseLiquidityParams({
tokenId: tokenId,
liquidity: liq,
amount0Min: (exp0 * (BPS - o.slippageBps)) / BPS,
amount1Min: (exp1 * (BPS - o.slippageBps)) / BPS,
deadline: block.timestamp
})
);
// principal + all accrued fees, straight to the position owner —
// there is no code path that pays anyone else
(uint256 amount0, uint256 amount1) = npm.collect(
INonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: owner_,
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
})
);
emit OrderExecuted(tokenId, owner_, spotTick, amount0, amount1);
}
/// Adaptive-window TWAP tick. Uses the largest window the pool's oracle
/// can answer (so it never reverts 'OLD'), capped at TWAP_MAX and floored
/// at TWAP_MIN — under the floor it reverts StaleOracle (fail closed) so a
/// brand-new pool cannot trigger on a meaninglessly short average.
function _consultTwap(IUniswapV3Pool pool) internal view returns (int24) {
uint32 window = _availableWindow(pool);
if (window < TWAP_MIN) revert StaleOracle();
uint32[] memory ago = new uint32[](2);
ago[0] = window;
ago[1] = 0;
(int56[] memory cum,) = pool.observe(ago);
int56 delta = cum[1] - cum[0];
int24 tick = int24(delta / int56(uint56(window)));
// round toward negative infinity, matching Uniswap's OracleLibrary
if (delta < 0 && (delta % int56(uint56(window)) != 0)) tick--;
return tick;
}
/// Seconds of history actually available, capped at TWAP_MAX. Mirrors
/// OracleLibrary.getOldestObservationSecondsAgo: the slot after the current
/// index holds the oldest observation once the ring is full; before that
/// the oldest is index 0.
function _availableWindow(IUniswapV3Pool pool) internal view returns (uint32) {
(,, uint16 index, uint16 cardinality,,,) = pool.slot0();
if (cardinality == 0) return 0;
uint256 oldestIndex = (uint256(index) + 1) % cardinality;
(uint32 ts,,, bool initialized) = pool.observations(oldestIndex);
if (!initialized) {
(ts,,,) = pool.observations(0);
}
// wrap deliberately, as Uniswap's oracle does: the uint32 second clock
// overflows by design, and elapsed = now - ts stays correct across it
uint32 age;
unchecked {
age = uint32(block.timestamp) - ts;
}
return age > TWAP_MAX ? TWAP_MAX : age;
}
}
Chain explorer3990msChain node87ms