Contract detail
ArcusSettlement
ArcusSettlement is a verified Robinhood Chain smart contract at 0x28fe3236…7da89457 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
- 0xb72877df…51940312
- Created at block
- #50528
- 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 {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.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";
import {IArcusExecutor} from "./interfaces/IArcusExecutor.sol";
import {IPermit2} from "./interfaces/IPermit2.sol";
import {WrappedTokenEscrow} from "./wrapped/WrappedTokenEscrow.sol";
import {WrappedTokenFactory} from "./wrapped/WrappedTokenFactory.sol";
/**
* @title ArcusSettlement
* @notice Executes a taker-signed `TakerIntent` through an Arcus-built route of
* allowlisted executors. The transaction is broadcast by a whitelisted
* Arcus operator.
*
* @dev v1 design (see ArcusSettlementSpec.md):
* - The taker order is signed as a Permit2 witness. There is no settlement-
* native EIP-712 domain and no settlement-owned replay state.
* - Settlement never emits arbitrary calldata. Every liquidity source is
* driven through the pinned IArcusExecutor.execute interface.
* - Accounting is by balance delta, never executor-reported amounts.
* - No native ETH, no standing allowances, and no partial fills.
*/
contract ArcusSettlement is OwnableUpgradeable, UUPSUpgradeable, ReentrancyGuard {
using SafeERC20 for IERC20;
// ── Types ────────────────────────────────────────────────────────────────
/// @notice The economic terms the taker signs as a Permit2 witness.
struct TakerIntent {
address taker;
address takerSellToken;
address takerBuyToken;
uint256 sellAmount;
uint256 minBuyAmount;
bool allowWrapped; // taker opts in to settling in the wrapped representation of takerBuyToken
uint256 nonce; // Permit2 nonce
uint256 deadline; // Permit2 deadline
}
/// @notice A single route hop driven through the pinned executor interface.
struct ExecutorHop {
address executor;
address inputToken;
uint256 inputAmount;
address outputToken;
uint256 minOutputAmount;
bytes executorData;
}
// ── Constants ──────────────────────────────────────────────────────────────
/// @notice Canonical Permit2 (Uniswap) singleton — same address on every chain.
IPermit2 public constant PERMIT2 = IPermit2(0x000000000022D473030F116dDEE9F6B43aC78BA3);
bytes32 public constant TAKER_INTENT_TYPEHASH = keccak256(
"TakerIntent(address taker,address takerSellToken,address takerBuyToken,uint256 sellAmount,uint256 minBuyAmount,bool allowWrapped,uint256 nonce,uint256 deadline)"
);
/// @notice Permit2 witness type string for the taker `TakerIntent` witness.
/// `TokenPermissions` is appended last per EIP-712 alphabetical ordering.
string public constant TAKER_INTENT_WITNESS_TYPE_STRING =
"TakerIntent witness)TakerIntent(address taker,address takerSellToken,address takerBuyToken,uint256 sellAmount,uint256 minBuyAmount,bool allowWrapped,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)";
// ── Storage ──────────────────────────────────────────────────────────────
/// @notice Whitelisted operators allowed to call `execute`.
mapping(address => bool) public approvedOperators;
/// @notice Allowlisted executors Settlement may drive through IArcusExecutor.
mapping(address => bool) public approvedExecutors;
/// @notice Canonical wrapped-token factory used to resolve the wrapped
/// representation of a taker's buy token when `allowWrapped` is set.
/// Security-critical: this address defines what counts as "wrapped".
/// `setWrappedTokenFactory` cross-checks the factory/escrow wiring.
WrappedTokenFactory public wrappedTokenFactory;
// Reserved storage slots for future upgrades; consume from the end by
// reducing the array length. Ownable/UUPS/ReentrancyGuard use namespaced
// storage and do not occupy sequential slots.
uint256[47] private _gap;
// ── Events ───────────────────────────────────────────────────────────────
event TakerIntentExecuted(
address indexed taker,
address indexed submitter,
address takerSellToken,
address takerBuyToken,
uint256 sellAmount,
uint256 buyAmount,
bool wrappedToken
);
event OperatorApproval(address indexed operator, bool allowed);
event ExecutorApproval(address indexed executor, bool allowed);
event WrappedTokenFactorySet(address indexed factory);
// ── Errors ───────────────────────────────────────────────────────────────
error OperatorNotApproved(address operator);
error ExecutorNotApproved(address executor);
error InvalidExecutor(address executor);
error InvalidWrappedTokenFactory(address factory);
error WrappedTokenFactoryNotSet();
error OrderExpired();
error InvalidTaker();
error InvalidToken();
error InvalidSellAmount();
error InvalidMinBuyAmount();
error SameToken();
error InvalidRouteShape();
error InsufficientHopOutput(uint256 received, uint256 minRequired);
error InsufficientOutput(uint256 received, uint256 minRequired);
// ── Initialization ─────────────────────────────────────────────────────────
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(address initialOwner) external initializer {
__Ownable_init(initialOwner);
}
function _authorizeUpgrade(address) internal override onlyOwner {}
// ── Admin ────────────────────────────────────────────────────────────────
/// @notice Grant or revoke an operator allowed to call `execute`.
function setOperator(address operator, bool allowed) external onlyOwner {
approvedOperators[operator] = allowed;
emit OperatorApproval(operator, allowed);
}
/// @notice Grant or revoke an executor allowed to be driven by settlement.
function setExecutor(address executor, bool allowed) external onlyOwner {
if (allowed && (executor == address(0) || executor.code.length == 0)) revert InvalidExecutor(executor);
approvedExecutors[executor] = allowed;
emit ExecutorApproval(executor, allowed);
}
/// @notice Set the canonical wrapped-token factory used to resolve the wrapped
/// representation of a taker's buy token for `allowWrapped` intents.
/// @dev Reverts unless the factory/escrow/settlement wiring is consistent.
/// `WrappedTokenRfqExecutor.initialize` enforces the executor side.
function setWrappedTokenFactory(address factory) external onlyOwner {
if (factory == address(0) || factory.code.length == 0) revert InvalidWrappedTokenFactory(factory);
WrappedTokenFactory wrappedFactory = WrappedTokenFactory(factory);
// Wrapped tokens only honor `transfer` from their delivery authority, so
// settlement must be that authority or delivery to the taker reverts.
if (wrappedFactory.deliveryAuthority() != address(this)) revert InvalidWrappedTokenFactory(factory);
// The authorized escrow is the sole minter of the factory's tokens; it must
// mint via this same factory and pay out to this settlement.
WrappedTokenEscrow escrow = WrappedTokenEscrow(wrappedFactory.authorizedEscrow());
if (
address(escrow) == address(0) || address(escrow.factory()) != factory
|| escrow.settlement() != address(this)
) revert InvalidWrappedTokenFactory(factory);
wrappedTokenFactory = wrappedFactory;
emit WrappedTokenFactorySet(factory);
}
// ── Settlement ───────────────────────────────────────────────────────────
/**
* @notice Execute a taker intent through an Arcus-built route.
* @param intent The taker's signed economic terms (the taker Permit2 witness).
* @param takerPermit2Sig The taker's Permit2 `permitWitnessTransferFrom` signature.
* @param hops One-or-more executor hops. The first hop consumes taker sell
* token; later hops consume the full measured output from the prior hop.
* @return boughtAmount The total settled buy token received, by balance delta.
* The settled token is `takerBuyToken`, or possibly the wrapped
* representation when the taker signed `allowWrapped == true`.
*/
function execute(TakerIntent calldata intent, bytes calldata takerPermit2Sig, ExecutorHop[] calldata hops)
external
nonReentrant
returns (uint256 boughtAmount)
{
if (!approvedOperators[msg.sender]) revert OperatorNotApproved(msg.sender);
_validateIntent(intent);
_validateRouteShape(intent, hops);
// The settled output token is the validated final hop output: either the
// signed buy token, or (for allowWrapped intents) its wrapped representation.
address settledToken = hops[hops.length - 1].outputToken;
bool wrappedToken = settledToken != intent.takerBuyToken;
IERC20 buyToken = IERC20(settledToken);
IERC20 sellToken = IERC20(intent.takerSellToken);
uint256 buyBefore = _outputBalanceOfMaybeUndeployed(settledToken, address(this));
uint256 sellBefore = sellToken.balanceOf(address(this));
_pullTaker(intent, takerPermit2Sig);
_executeHops(intent, hops);
// ── Final output accounting (balance delta, not route-reported) ──────
boughtAmount = _outputBalanceOfMaybeUndeployed(settledToken, address(this)) - buyBefore;
if (boughtAmount < intent.minBuyAmount) revert InsufficientOutput(boughtAmount, intent.minBuyAmount);
// Send all measured output to the taker while enforcing the signed floor.
uint256 takerBefore = buyToken.balanceOf(intent.taker);
buyToken.safeTransfer(intent.taker, boughtAmount);
uint256 takerDelta = buyToken.balanceOf(intent.taker) - takerBefore;
if (takerDelta < intent.minBuyAmount) revert InsufficientOutput(takerDelta, intent.minBuyAmount);
// ── Refund unspent takerSellToken to the payer (the taker) ───────────
uint256 sellLeftover = sellToken.balanceOf(address(this)) - sellBefore;
if (sellLeftover != 0) {
sellToken.safeTransfer(intent.taker, sellLeftover);
}
emit TakerIntentExecuted(
intent.taker,
msg.sender,
intent.takerSellToken,
intent.takerBuyToken,
intent.sellAmount,
boughtAmount,
wrappedToken
);
}
// ── Internal: validation ───────────────────────────────────────────────────
function _validateIntent(TakerIntent calldata intent) internal view {
if (block.timestamp > intent.deadline) revert OrderExpired();
if (intent.taker == address(0)) revert InvalidTaker();
if (intent.takerSellToken == address(0) || intent.takerBuyToken == address(0)) revert InvalidToken();
if (intent.takerSellToken == intent.takerBuyToken) revert SameToken();
if (intent.sellAmount == 0) revert InvalidSellAmount();
if (intent.minBuyAmount == 0) revert InvalidMinBuyAmount();
}
function _validateRouteShape(TakerIntent calldata intent, ExecutorHop[] calldata hops) internal view {
uint256 len = hops.length;
if (len == 0) revert InvalidRouteShape();
if (hops[0].inputToken != intent.takerSellToken) revert InvalidRouteShape();
if (hops[0].inputAmount == 0) revert InvalidRouteShape();
if (hops[0].inputAmount > intent.sellAmount) revert InvalidRouteShape();
// The final output must be the signed buy token. When the taker opts into
// wrapped settlement, the computed wrapped representation is also accepted.
address finalOutput = hops[len - 1].outputToken;
if (intent.allowWrapped) {
WrappedTokenFactory factory = wrappedTokenFactory;
if (address(factory) == address(0)) revert WrappedTokenFactoryNotSet();
if (finalOutput != intent.takerBuyToken && finalOutput != factory.predictWrappedToken(intent.takerBuyToken))
{
revert InvalidRouteShape();
}
} else if (finalOutput != intent.takerBuyToken) {
revert InvalidRouteShape();
}
for (uint256 i; i < len; ++i) {
ExecutorHop calldata hop = hops[i];
if (hop.executor == address(0) || hop.inputToken == address(0) || hop.outputToken == address(0)) {
revert InvalidRouteShape();
}
if (!approvedExecutors[hop.executor]) revert ExecutorNotApproved(hop.executor);
if (i != 0 && hop.inputToken != hops[i - 1].outputToken) revert InvalidRouteShape();
}
}
// ── Internal: Permit2 pulls ────────────────────────────────────────────────
function _pullTaker(TakerIntent calldata intent, bytes calldata signature) internal {
PERMIT2.permitWitnessTransferFrom(
IPermit2.PermitTransferFrom({
permitted: IPermit2.TokenPermissions({token: intent.takerSellToken, amount: intent.sellAmount}),
nonce: intent.nonce,
deadline: intent.deadline
}),
IPermit2.SignatureTransferDetails({to: address(this), requestedAmount: intent.sellAmount}),
intent.taker,
_hashTakerIntent(intent),
TAKER_INTENT_WITNESS_TYPE_STRING,
signature
);
}
function _executeHops(TakerIntent calldata intent, ExecutorHop[] calldata hops) internal {
uint256 amount = hops[0].inputAmount;
uint256 len = hops.length;
for (uint256 i; i < len; ++i) {
ExecutorHop calldata hop = hops[i];
if (i != 0 && hop.inputAmount != amount) revert InvalidRouteShape();
IERC20(hop.inputToken).safeTransfer(hop.executor, amount);
uint256 outputBefore = _outputBalanceOfMaybeUndeployed(hop.outputToken, address(this));
IArcusExecutor(hop.executor)
.execute(intent.taker, hop.inputToken, amount, hop.outputToken, hop.minOutputAmount, hop.executorData);
amount = _outputBalanceOfMaybeUndeployed(hop.outputToken, address(this)) - outputBefore;
if (amount < hop.minOutputAmount) revert InsufficientHopOutput(amount, hop.minOutputAmount);
}
}
// @notice this is a helper function to get the balance of a token, it will return 0 if the token is not deployed.
function _outputBalanceOfMaybeUndeployed(address token, address account) internal view returns (uint256) {
if (token.code.length == 0) return 0;
return IERC20(token).balanceOf(account);
}
// ── Witness hashing (domainless struct hashes; Permit2 wraps the domain) ───
/// @notice The `TakerIntent` witness struct hash an off-chain taker commits to.
function hashTakerIntent(TakerIntent calldata intent) external pure returns (bytes32) {
return _hashTakerIntent(intent);
}
function _hashTakerIntent(TakerIntent calldata intent) internal pure returns (bytes32) {
return keccak256(
abi.encode(
TAKER_INTENT_TYPEHASH,
intent.taker,
intent.takerSellToken,
intent.takerBuyToken,
intent.sellAmount,
intent.minBuyAmount,
intent.allowWrapped,
intent.nonce,
intent.deadline
)
);
}
}