Contract detail
EquifoldV4CustomSwapRouter
Live Robinhood Chain account facts for 0x04c817b7…ab170d16 with at least 25 observed transactions. The history total is updating as indexing continues.
- Address type
- Smart contract
- Transactions
- 25+ observed
- First indexed block
- #60016243
- Latest indexed block
- #82382876
- History coverage
- Recent activity available; total is catching up
- Indexed token transfers
- 14
- ERC-20 transfers
- 14
- Verification
- Verified
- Compiler
- 0.8.24+commit.e11b9ed9
- Verification match
- partial
- Language
- Solidity
- EVM version
- cancun
- License
- MIT
- Compiler optimization
- Enabled · 10000 runs
- Deployed bytecode hash
- 0x3cb77e7b3481d95890087cb3d1fe617687ec5deea0c914468c909b7408a8041f
Related activity
Transactions
Verified contract
Contract source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { IPoolManager } from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import { IUnlockCallback } from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import { PoolKey } from "@uniswap/v4-core/src/types/PoolKey.sol";
import { Currency } from "@uniswap/v4-core/src/types/Currency.sol";
import { BalanceDelta, BalanceDeltaLibrary } from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import { SwapParams } from "@uniswap/v4-core/src/types/PoolOperation.sol";
import { TickMath } from "@uniswap/v4-core/src/libraries/TickMath.sol";
interface IEquifoldV4CustomModuleFactory {
function poolKeyOf(
address token
) external view returns (PoolKey memory);
function quoteOf(
address token
) external view returns (address);
}
/// @title Equifold direct custom-quote swap router
/// @notice Trades a custom-paired launch token directly against its selected ERC-20 quote. It does
/// not silently bridge through USDG: every pool keeps its own volume and fee denomination.
contract EquifoldV4CustomSwapRouter is IUnlockCallback, ReentrancyGuard {
using SafeERC20 for IERC20;
using BalanceDeltaLibrary for BalanceDelta;
error AmountOverflow();
error DeadlineExpired();
error IncorrectCaller();
error InvalidAddress();
error InvalidRoute();
error SlippageExceeded();
error UnknownToken();
error ZeroAmount();
error ZeroOutput();
uint256 private constant MAX_EXACT_IN_AMOUNT = (1 << 255) - 1;
struct CallbackData {
PoolKey key;
bool zeroForOne;
uint256 amountIn;
Currency input;
Currency output;
address recipient;
}
IPoolManager public immutable poolManager;
IEquifoldV4CustomModuleFactory public immutable moduleFactory;
constructor(
IPoolManager poolManager_,
address moduleFactory_
) {
if (address(poolManager_) == address(0) || moduleFactory_ == address(0)) {
revert InvalidAddress();
}
if (address(poolManager_).code.length == 0 || moduleFactory_.code.length == 0) {
revert InvalidAddress();
}
poolManager = poolManager_;
moduleFactory = IEquifoldV4CustomModuleFactory(moduleFactory_);
}
function buyWithQuote(
address token,
uint256 amountIn,
uint256 minOut,
address recipient,
uint256 deadline
) external nonReentrant returns (uint256 amountOut) {
_guard(deadline, recipient, amountIn);
(PoolKey memory key, address quote) = _launch(token);
IERC20(quote).safeTransferFrom(msg.sender, address(this), amountIn);
amountOut = _run(key, Currency.wrap(quote), amountIn, recipient);
if (amountOut < minOut) revert SlippageExceeded();
}
function sellToQuote(
address token,
uint256 amountIn,
uint256 minOut,
address recipient,
uint256 deadline
) external nonReentrant returns (uint256 amountOut) {
_guard(deadline, recipient, amountIn);
(PoolKey memory key,) = _launch(token);
IERC20(token).safeTransferFrom(msg.sender, address(this), amountIn);
amountOut = _run(key, Currency.wrap(token), amountIn, recipient);
if (amountOut < minOut) revert SlippageExceeded();
}
function unlockCallback(
bytes calldata rawData
) external returns (bytes memory) {
if (msg.sender != address(poolManager)) revert IncorrectCaller();
CallbackData memory d = abi.decode(rawData, (CallbackData));
BalanceDelta delta = poolManager.swap(
d.key,
SwapParams({
zeroForOne: d.zeroForOne,
amountSpecified: _negatedAmountSpecified(d.amountIn),
sqrtPriceLimitX96: d.zeroForOne
? TickMath.MIN_SQRT_PRICE + 1
: TickMath.MAX_SQRT_PRICE - 1
}),
""
);
int128 inputDelta = d.zeroForOne ? delta.amount0() : delta.amount1();
int128 outputDelta = d.zeroForOne ? delta.amount1() : delta.amount0();
if (inputDelta >= 0 || outputDelta <= 0) revert ZeroOutput();
// Settle the delta the PoolManager actually consumed. Paying the
// caller-declared exact-in amount directly is unsafe at a price limit,
// where a swap may consume less than requested.
uint256 inputOwed = uint256(uint128(-inputDelta));
uint256 amountOut = uint256(uint128(outputDelta));
poolManager.sync(d.input);
IERC20(Currency.unwrap(d.input)).safeTransfer(address(poolManager), inputOwed);
poolManager.settle();
poolManager.take(d.output, d.recipient, amountOut);
return abi.encode(amountOut, inputOwed);
}
function _launch(
address token
) private view returns (PoolKey memory key, address quote) {
key = moduleFactory.poolKeyOf(token);
quote = moduleFactory.quoteOf(token);
if (quote == address(0)) revert UnknownToken();
}
function _run(
PoolKey memory key,
Currency input,
uint256 amountIn,
address recipient
) private returns (uint256 amountOut) {
if (amountIn > MAX_EXACT_IN_AMOUNT) revert AmountOverflow();
bool zeroForOne;
Currency output;
if (Currency.unwrap(key.currency0) == Currency.unwrap(input)) {
zeroForOne = true;
output = key.currency1;
} else if (Currency.unwrap(key.currency1) == Currency.unwrap(input)) {
zeroForOne = false;
output = key.currency0;
} else {
revert InvalidRoute();
}
uint256 amountConsumed;
(amountOut, amountConsumed) = abi.decode(
poolManager.unlock(
abi.encode(
CallbackData({
key: key,
zeroForOne: zeroForOne,
amountIn: amountIn,
input: input,
output: output,
recipient: recipient
})
)
),
(uint256, uint256)
);
// An exact-input swap can stop at the extreme price limit after using
// only part of the requested amount. Return that unconsumed input to
// the caller instead of leaving it stranded in this stateless router.
if (amountConsumed < amountIn) {
IERC20(Currency.unwrap(input)).safeTransfer(msg.sender, amountIn - amountConsumed);
}
}
function _guard(
uint256 deadline,
address recipient,
uint256 amountIn
) private view {
if (block.timestamp > deadline) revert DeadlineExpired();
if (recipient == address(0)) revert InvalidAddress();
if (amountIn == 0) revert ZeroAmount();
}
function _negatedAmountSpecified(
uint256 amountIn
) private pure returns (int256) {
if (amountIn > MAX_EXACT_IN_AMOUNT) revert AmountOverflow();
return -int256(amountIn);
}
}