Robinhood MainnetChain ID: 4663
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
            )
        );
    }
}