Contract detail
SwapShell
SwapShell is a verified Robinhood Chain smart contract at 0x1d482c4a…97c788c4 with 0 indexed transactions.
- Address type
- Smart contract
- Transactions
- 0
- History coverage
- No indexed activity found
- Emitted events
- 1
- Verification
- Verified
- Compiler
- v0.8.27+commit.40a35a09
- Verification match
- full
- Creation transaction
- 0x00f7db77…6cb5d466
- Created at block
- #50527
- Language
- solidity
- EVM version
- prague
- License
- none
- Compiler optimization
- Enabled · 200 runs
Verified contract
Contract source code
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
interface IZeroExSettlerRegistry {
function ownerOf(uint256 tokenId) external view returns (address);
}
/**
* @title SwapShell
* @notice Pass-through wrapper around Permit2-native swap venues. Emits one
* normalized `SwapExecuted` event regardless of which venue (Bebop
* JAM, 0x v2 Settler, our own IntentSettlement, Uniswap UniversalRouter,
* ...) actually executed the trade. A frontend/indexer only needs to
* watch one log type.
*
* Architecture:
* - SwapShell does NOT pull tokens. The downstream `router` is expected to be
* Permit2-native: it takes the user's Permit2 signature in its own calldata
* and calls `Permit2.permitWitnessTransferFrom` itself to pull `tokenIn`.
* - SwapShell does NOT verify the user's authorization. That happens inside
* the router (which validates the Permit2 witness and the order signature).
* A bogus calldata simply reverts at the inner call.
* - SwapShell DOES verify the swap actually delivered tokenOut to `meta.taker`
* by snapshotting their balance pre/post and enforcing `minAmountOut`. This
* catches misrouted calldata (delta == 0 => revert) and gives a reliable
* `amountOut` to put in the event.
* - SwapShell DOES allow folding a one-time EIP-2612 permit (token -> Permit2)
* into the same tx, so a fresh taker can do their first swap in a single
* gasless submission.
*
*/
contract SwapShell is ReentrancyGuard, OwnableUpgradeable, UUPSUpgradeable {
address public constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
address private constant ZEROEX_SETTLER_REGISTRY = 0x00000000000004533Fe15556B1E086BB1A72cEae;
uint256 private constant ZEROEX_METATXN_TOKEN_ID = 3;
bytes32 private constant ZEROEX_ROUTE_TAG = bytes32("ZEROEX");
/// Function selector for Error(string)
bytes4 private constant ERROR_STRING_SELECTOR = 0x08c379a0;
bytes4 private constant PANIC_SELECTOR = 0x4e487b71;
/// @notice Caller-provided swap metadata used to validate the router call and normalize the emitted event.
/// @param taker Address whose tokenOut balance is measured; must match the recipient encoded in router calldata.
/// @param router Downstream swap venue to call; must be allowlisted.
/// @param tokenIn Input token for the swap.
/// @param tokenOut Output token expected by the taker.
/// @param amountIn Input amount surfaced in the normalized event.
/// @param quotedAmountIn Operator-supplied quoted input amount surfaced in the normalized event.
/// @param quotedAmountOut Operator-supplied quoted output amount surfaced in the normalized event.
/// @param minAmountOut Minimum tokenOut balance delta required for the swap to succeed.
/// @param tokenInBenchmarkPrice UI-supplied benchmark price for tokenIn, scaled by 1e18.
/// @param tokenOutBenchmarkPrice UI-supplied benchmark price for tokenOut, scaled by 1e18.
/// @param routeTag Free-form route identifier surfaced in the event, e.g. "INTENT", "0X", or "BEBOP".
struct SwapMeta {
address taker;
address router;
address tokenIn;
address tokenOut;
uint256 amountIn;
uint256 quotedAmountIn;
uint256 quotedAmountOut;
uint256 minAmountOut;
uint256 tokenInBenchmarkPrice;
uint256 tokenOutBenchmarkPrice;
bytes32 routeTag;
}
/// @notice Optional EIP-2612 permit applied before the router call.
/// Spender is fixed to Permit2 -- this is purely for one-time
/// token -> Permit2 allowance enablement.
struct PermitData {
address token;
uint256 value;
uint256 deadline;
uint8 v;
bytes32 r;
bytes32 s;
}
mapping(address => bool) public approvedRouters;
mapping(address => bool) public approvedOperators;
// Reserved storage slots for future upgrades; consume from the end by reducing the array length.
// https://docs.openzeppelin.com/upgrades-plugins/writing-upgradeable#storage-gaps
uint256[48] private _gap;
event SwapExecuted(
address indexed taker,
address indexed tokenIn,
address indexed tokenOut,
uint256 minAmountOut,
uint256 amountIn,
uint256 quotedAmountIn,
uint256 quotedAmountOut,
uint256 amountOut,
uint256 tokenInBenchmarkPrice,
uint256 tokenOutBenchmarkPrice,
address router,
bytes32 routeTag,
bool success,
string reason
);
event RouterApproval(address indexed router, bool allowed);
event OperatorApproval(address indexed operator, bool allowed);
error RouterNotApproved(address router);
error OperatorNotApproved(address operator);
error BalanceDecreased(uint256 beforeBalance, uint256 afterBalance);
error MinOutNotMet(uint256 received, uint256 minRequired);
error RouterCallFailed(bytes returnData);
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(address initialOwner) external initializer {
__Ownable_init(initialOwner);
}
function _authorizeUpgrade(address) internal override onlyOwner {}
modifier isOperator() {
_checkOperator();
_;
}
function _checkOperator() internal view {
if (!approvedOperators[msg.sender]) revert OperatorNotApproved(msg.sender);
}
function setRouter(address router, bool allowed) external onlyOwner {
approvedRouters[router] = allowed;
emit RouterApproval(router, allowed);
}
function setOperator(address operator, bool allowed) external onlyOwner {
approvedOperators[operator] = allowed;
emit OperatorApproval(operator, allowed);
}
/**
* @notice Forward a Permit2-native swap call to the downstream router and
* emit a normalized `SwapExecuted` event.
*
* @param meta Trade descriptors used to measure the swap and shape
* the event. `meta.taker` is the address we read
* balances against -- it must match the recipient
* encoded in `routerCalldata`.
* @param permits Optional EIP-2612 permits to apply before the
* router call (e.g. first-time token -> Permit2
* setup for a fresh taker).
* @param routerCalldata Raw calldata forwarded as-is to `meta.router`.
* Carries the user's Permit2 signature and the
* rest of the venue-specific authorization.
*
* Safety:
* - Allowlist gate on `meta.router` -- `target.call(arbitrary)` is
* dangerous against unknown contracts (could be a token approving an
* attacker, etc.).
* - Balance delta check on taker's tokenOut catches a misrouted call
* (proceeds sent to the wrong address => delta 0 => revert).
*/
function swap(SwapMeta calldata meta, PermitData[] calldata permits, bytes calldata routerCalldata)
external
nonReentrant
isOperator
returns (uint256 amountOut)
{
if (meta.routeTag == ZEROEX_ROUTE_TAG) {
if (meta.router != IZeroExSettlerRegistry(ZEROEX_SETTLER_REGISTRY).ownerOf(ZEROEX_METATXN_TOKEN_ID)) {
revert RouterNotApproved(meta.router);
}
} else if (!approvedRouters[meta.router]) {
revert RouterNotApproved(meta.router);
}
// Apply any supplied EIP-2612 permits before the router attempts to pull funds.
for (uint256 i = 0; i < permits.length; i++) {
PermitData calldata p = permits[i];
IERC20Permit(p.token).permit(meta.taker, PERMIT2, p.value, p.deadline, p.v, p.r, p.s);
}
uint256 tokenInBalBefore = IERC20(meta.tokenIn).balanceOf(meta.taker);
uint256 tokenOutBalBefore = _balanceOfMaybeUndeployed(meta.tokenOut, meta.taker);
(bool ok, bytes memory ret) = meta.router.call(routerCalldata);
uint256 tokenInBalAfter = IERC20(meta.tokenIn).balanceOf(meta.taker);
uint256 tokenOutBalAfter = _balanceOfMaybeUndeployed(meta.tokenOut, meta.taker);
if (!ok) {
// If the router reverted cleanly before moving balances, keep the tx successful and emit failure details.
if (tokenInBalAfter == tokenInBalBefore && tokenOutBalAfter == tokenOutBalBefore) {
emit SwapExecuted(
meta.taker,
meta.tokenIn,
meta.tokenOut,
meta.minAmountOut,
meta.amountIn,
meta.quotedAmountIn,
meta.quotedAmountOut,
0,
meta.tokenInBenchmarkPrice,
meta.tokenOutBenchmarkPrice,
meta.router,
meta.routeTag,
false,
_decodeRouterRevertReason(ret)
);
return 0;
}
// This should not happen: a reverting router should not also mutate the taker's balances.
revert RouterCallFailed(ret);
}
// This should not happen: a successful swap should not reduce the taker's tokenOut balance.
if (tokenOutBalAfter < tokenOutBalBefore) revert BalanceDecreased(tokenOutBalBefore, tokenOutBalAfter);
amountOut = tokenOutBalAfter - tokenOutBalBefore;
if (amountOut < meta.minAmountOut) revert MinOutNotMet(amountOut, meta.minAmountOut);
emit SwapExecuted(
meta.taker,
meta.tokenIn,
meta.tokenOut,
meta.minAmountOut,
meta.amountIn,
meta.quotedAmountIn,
meta.quotedAmountOut,
amountOut,
meta.tokenInBenchmarkPrice,
meta.tokenOutBenchmarkPrice,
meta.router,
meta.routeTag,
true,
""
);
}
/// @dev Reads an ERC20 balance, returning 0 when `token` has no code yet.
/// A route may deploy `tokenOut` on the fly (e.g. a wrapped token
/// created during settlement), so the pre-route snapshot must not
/// revert on a codeless address. After the router call the token
/// exists and the post-route balance is read normally.
function _balanceOfMaybeUndeployed(address token, address account) internal view returns (uint256) {
if (token.code.length == 0) return 0;
return IERC20(token).balanceOf(account);
}
/// @dev Decodes Solidity's standard revert payloads. `revert("reason")`
/// is encoded as `Error(string)`, while `assert` and arithmetic
/// panics are encoded as `Panic(uint256)`. Custom errors are left as
/// a generic router failure because their ABI is venue-specific.
function _decodeRouterRevertReason(bytes memory returnData) private pure returns (string memory) {
if (returnData.length == 0) return "Router call reverted";
bytes4 selector;
assembly {
selector := mload(add(returnData, 32))
}
if (selector == ERROR_STRING_SELECTOR) {
return _decodeErrorString(returnData);
}
if (selector == PANIC_SELECTOR) return "Router call panicked";
return "Router call reverted";
}
function _decodeErrorString(bytes memory returnData) private pure returns (string memory) {
if (returnData.length < 68) return "Router call reverted";
uint256 offset;
uint256 reasonLength;
assembly {
offset := mload(add(returnData, 36))
reasonLength := mload(add(returnData, 68))
}
if (offset != 32 || reasonLength > returnData.length - 68) {
return "Router call reverted";
}
bytes memory reasonBytes = new bytes(reasonLength);
for (uint256 i = 0; i < reasonLength; i++) {
reasonBytes[i] = returnData[i + 68];
}
return string(reasonBytes);
}
}