V4PensionSwapRouter
0x0a314bb788bedd98d4dbdffe9adf5878097a4e40
Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
5,548 bytes
ABI entries
21
8 read · 3 write
License
none
Contract information
- Address
- 0x0a314bb788bedd98d4dbdffe9adf5878097a4e40
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.28+commit.7893614a
- Optimization
- Enabled
- Creator
- 0x907Dbe35f1…24570010fb
- Creation tx
- 0x6ef1be67ae…df79ae4709
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (8)
BIPS() → uint256
TAX_BIPS() → uint256
fee() → uint24
hook() → address
manager() → address
pension() → address
poolKey() → tuple
tickSpacing() → int24
Events (1)
RouterSwap
ABI
[
{
"inputs": [
{
"internalType": "contract IPoolManager",
"name": "poolManager",
"type": "address"
},
{
"internalType": "contract IERC20",
"name": "pensionToken",
"type": "address"
},
{
"internalType": "contract IHooks",
"name": "pensionHook",
"type": "address"
},
{
"internalType": "uint24",
"name": "poolFee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "poolTickSpacing",
"type": "int24"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "InsufficientOutput",
"type": "error"
},
{
"inputs": [],
"name": "InvalidAmount",
"type": "error"
},
{
"inputs": [],
"name": "InvalidPool",
"type": "error"
},
{
"inputs": [],
"name": "NotPoolManager",
"type": "error"
},
{
"inputs": [],
"name": "RefundFailed",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "payer",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": true,
"internalType": "bool",
"name": "buy",
"type": "bool"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amountIn",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amountOut",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "taxEth",
"type": "uint256"
}
],
"name": "RouterSwap",
"type": "event"
},
{
"inputs": [],
"name": "BIPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TAX_BIPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"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": "minPensionOut",
"type": "uint256"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "buy",
"outputs": [
{
"internalType": "uint256",
"name": "pensionOut",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "fee",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "hook",
"outputs": [
{
"internalType": "contract IHooks",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "manager",
"outputs": [
{
"internalType": "contract IPoolManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pension",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolKey",
"outputs": [
{
"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": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"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": "pensionIn",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minEthOut",
"type": "uint256"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "sell",
"outputs": [
{
"internalType": "uint256",
"name": "ethOut",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "tickSpacing",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "rawData",
"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 {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {IUnlockCallback} from "v4-core/interfaces/callback/IUnlockCallback.sol";
import {IHooks} from "v4-core/interfaces/IHooks.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";
import {BalanceDelta} from "v4-core/types/BalanceDelta.sol";
import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {TransientStateLibrary} from "v4-core/libraries/TransientStateLibrary.sol";
contract V4PensionSwapRouter is IUnlockCallback {
using CurrencyLibrary for Currency;
using SafeERC20 for IERC20;
using TransientStateLibrary for IPoolManager;
uint256 public constant TAX_BIPS = 400;
uint256 public constant BIPS = 10_000;
IPoolManager public immutable manager;
IERC20 public immutable pension;
IHooks public immutable hook;
uint24 public immutable fee;
int24 public immutable tickSpacing;
enum Action {
Buy,
Sell
}
struct CallbackData {
Action action;
address payer;
address recipient;
uint256 amountIn;
}
event RouterSwap(
address indexed payer,
address indexed recipient,
bool indexed buy,
uint256 amountIn,
uint256 amountOut,
uint256 taxEth
);
error InsufficientOutput();
error InvalidAmount();
error InvalidPool();
error NotPoolManager();
error RefundFailed();
error ZeroAddress();
constructor(
IPoolManager poolManager,
IERC20 pensionToken,
IHooks pensionHook,
uint24 poolFee,
int24 poolTickSpacing
) {
manager = poolManager;
pension = pensionToken;
hook = pensionHook;
fee = poolFee;
tickSpacing = poolTickSpacing;
}
receive() external payable {}
function poolKey() public view returns (PoolKey memory) {
return PoolKey({
currency0: CurrencyLibrary.ADDRESS_ZERO,
currency1: Currency.wrap(address(pension)),
fee: fee,
tickSpacing: tickSpacing,
hooks: hook
});
}
function buy(PoolKey calldata key, uint256 minPensionOut, address recipient)
external
payable
returns (uint256 pensionOut)
{
_validatePoolKey(key);
(pensionOut,) = _buyExactEthForPension(minPensionOut, recipient);
}
function sell(PoolKey calldata key, uint256 pensionIn, uint256 minEthOut, address recipient)
external
returns (uint256 ethOut)
{
_validatePoolKey(key);
(ethOut,) = _sellExactPensionForEth(pensionIn, minEthOut, recipient);
}
function _buyExactEthForPension(uint256 minPensionOut, address recipient)
internal
returns (uint256 pensionOut, uint256 taxEth)
{
if (recipient == address(0)) revert ZeroAddress();
if (msg.value == 0) revert InvalidAmount();
(pensionOut, taxEth) = abi.decode(
manager.unlock(abi.encode(CallbackData(Action.Buy, msg.sender, recipient, msg.value))), (uint256, uint256)
);
if (pensionOut < minPensionOut) revert InsufficientOutput();
_refundEth(msg.sender);
emit RouterSwap(msg.sender, recipient, true, msg.value, pensionOut, taxEth);
}
function _sellExactPensionForEth(uint256 pensionIn, uint256 minEthOut, address recipient)
internal
returns (uint256 ethOut, uint256 taxEth)
{
if (recipient == address(0)) revert ZeroAddress();
if (pensionIn == 0) revert InvalidAmount();
(ethOut, taxEth) = abi.decode(
manager.unlock(abi.encode(CallbackData(Action.Sell, msg.sender, recipient, pensionIn))), (uint256, uint256)
);
if (ethOut < minEthOut) revert InsufficientOutput();
_refundEth(msg.sender);
emit RouterSwap(msg.sender, recipient, false, pensionIn, ethOut, taxEth);
}
function unlockCallback(bytes calldata rawData) external returns (bytes memory) {
if (msg.sender != address(manager)) revert NotPoolManager();
CallbackData memory data = abi.decode(rawData, (CallbackData));
PoolKey memory key = poolKey();
IPoolManager.SwapParams memory params = data.action == Action.Buy
? IPoolManager.SwapParams({
zeroForOne: true,
amountSpecified: -int256(data.amountIn),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
})
: IPoolManager.SwapParams({
zeroForOne: false,
amountSpecified: -int256(data.amountIn),
sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1
});
manager.swap(key, params, "");
int256 delta0 = manager.currencyDelta(address(this), key.currency0);
int256 delta1 = manager.currencyDelta(address(this), key.currency1);
if (delta0 < 0) manager.settle{value: uint256(-delta0)}();
if (delta1 < 0) {
uint256 tokenAmount = uint256(-delta1);
manager.sync(key.currency1);
pension.safeTransferFrom(data.payer, address(manager), tokenAmount);
manager.settle();
}
uint256 ethOut;
uint256 pensionOut;
if (delta0 > 0) {
ethOut = uint256(delta0);
manager.take(key.currency0, data.recipient, ethOut);
}
if (delta1 > 0) {
pensionOut = uint256(delta1);
manager.take(key.currency1, data.recipient, pensionOut);
}
uint256 taxEth =
data.action == Action.Buy ? data.amountIn * TAX_BIPS / BIPS : ethOut * TAX_BIPS / (BIPS - TAX_BIPS);
return abi.encode(data.action == Action.Buy ? pensionOut : ethOut, taxEth);
}
function _validatePoolKey(PoolKey calldata key) internal view {
PoolKey memory expected = poolKey();
if (
!(key.currency0 == expected.currency0) || !(key.currency1 == expected.currency1) || key.fee != expected.fee
|| key.tickSpacing != expected.tickSpacing || address(key.hooks) != address(expected.hooks)
) {
revert InvalidPool();
}
}
function _refundEth(address to) internal {
uint256 refund = address(this).balance;
if (refund == 0) return;
(bool success,) = payable(to).call{value: refund}("");
if (!success) revert RefundFailed();
}
}
Chain explorer3204msChain node89ms