Robinhood MainnetChain ID: 4663
Contract detail

WrappedTokenEscrow

WrappedTokenEscrow is a verified Robinhood Chain smart contract at 0x1b20f2e4…cd514e51 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
0xd1d9e9a0…c669b5a8
Created at block
#50534
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 {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import {IPermit2} from "../interfaces/IPermit2.sol";
import {WrappedToken} from "./WrappedToken.sol";
import {WrappedTokenFactory} from "./WrappedTokenFactory.sol";

/// @notice Custody and lifecycle contract for wrapped RFQ fills.
contract WrappedTokenEscrow is OwnableUpgradeable, UUPSUpgradeable {
    using SafeERC20 for IERC20;
    using EnumerableSet for EnumerableSet.Bytes32Set;

    IPermit2 public constant PERMIT2 = IPermit2(0x000000000022D473030F116dDEE9F6B43aC78BA3);

    uint256 public constant MIN_WINDOW = 10 minutes;
    uint256 public constant MAX_WINDOW = 5 days;
    bytes32 public constant WRAPPED_ORDER_ID_TYPEHASH = keccak256("ArcusWrappedOrder(bytes32 quoteHash)");

    // Maker settlement authorization signed as a Permit2 witness over the real underlying.
    bytes32 public constant WRAPPED_SETTLEMENT_AUTHORIZATION_TYPEHASH = keccak256(
        "WrappedSettlementAuthorization(bytes32 orderId,address maker,address underlying,uint256 underlyingAmount,uint256 deadline)"
    );
    string public constant WRAPPED_SETTLEMENT_WITNESS_TYPE_STRING =
        "WrappedSettlementAuthorization witness)TokenPermissions(address token,uint256 amount)WrappedSettlementAuthorization(bytes32 orderId,address maker,address underlying,uint256 underlyingAmount,uint256 deadline)";

    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;

    struct MintRecord {
        address maker;
        address taker;
        address underlying;
        address takerSellToken;
        uint256 wrappedTokenAmount;
        uint256 lockedSellAmount;
        uint256 settlementDeadline;
        bool settled;
    }

    WrappedTokenFactory public factory;
    address public settlement;
    address public executor;
    uint256 public settlementWindow;
    /// @notice When nonzero, new mints use this window instead of `settlementWindow` for `settlementDeadline`.
    ///         Bounded by `MIN_WINDOW`/`MAX_WINDOW`; exceeding requires a contract upgrade. 0 clears.
    uint256 public globalReclaimWindow;
    uint256 private _reentrancyStatus;

    mapping(address maker => bool) public approvedIssuers;
    mapping(address wrappedToken => mapping(address maker => uint256 cap)) private _mintageCap;
    mapping(address wrappedToken => mapping(address maker => uint256 amount)) public outstandingWrapped;
    mapping(bytes32 orderId => MintRecord record) public mintRecords;

    address public settlementOperator;

    /// @notice Live (minted, not yet settled or reclaimed) order ids per maker.
    ///         Maintained on mint/settle/reclaim so makers can enumerate their
    ///         outstanding orders off-chain without scanning event logs.
    mapping(address maker => EnumerableSet.Bytes32Set) private _makerOpenOrders;

    uint256[41] private _gap;

    event ExecutorSet(address indexed executor);
    event SettlementOperatorSet(address indexed settlementOperator);
    event IssuerApproval(address indexed maker, bool approved);
    event MintageCapSet(address indexed wrappedToken, address indexed maker, address indexed caller, uint256 cap);
    event SettlementWindowSet(uint256 settlementWindow);
    event GlobalReclaimWindowSet(uint256 globalReclaimWindow);
    event WrappedTokenMinted(
        bytes32 indexed orderId,
        address indexed maker,
        address indexed taker,
        address underlying,
        address wrappedToken,
        address takerSellToken,
        uint256 wrappedTokenAmount,
        uint256 lockedSellAmount,
        uint256 settlementDeadline
    );
    event WrappedTokenSettled(
        bytes32 indexed orderId,
        address indexed maker,
        address indexed taker,
        address caller,
        address takerSellToken,
        uint256 wrappedTokenAmount,
        uint256 releasedSellAmount
    );
    event WrappedTokenReclaimed(
        bytes32 indexed orderId,
        address indexed maker,
        address indexed taker,
        address caller,
        address takerSellToken,
        uint256 wrappedTokenAmount,
        uint256 returnedSellAmount
    );

    error InvalidAddress();
    error InvalidAmount();
    error InvalidWindow();
    error OnlyExecutor();
    error OnlyTakerOrOperator();
    error OnlyOperator();
    error IssuerNotApproved(address maker);
    error MintageCapExceeded(uint256 outstanding, uint256 amount, uint256 cap);
    error OrderAlreadyUsed(bytes32 orderId);
    error UnknownOrder(bytes32 orderId);
    error OrderTerminated(bytes32 orderId);
    error DeadlineNotReached(uint256 deadline);
    error BalanceDeltaMismatch(address token, uint256 expected, uint256 actual);
    error ReentrantCall();

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize(address factory_, address settlement_, address initialOwner, uint256 settlementWindow_)
        external
        initializer
    {
        if (factory_ == address(0) || settlement_ == address(0)) revert InvalidAddress();
        __Ownable_init(initialOwner);
        factory = WrappedTokenFactory(factory_);
        settlement = settlement_;
        _reentrancyStatus = _NOT_ENTERED;
        _setSettlementWindow(settlementWindow_);
    }

    function _authorizeUpgrade(address) internal override onlyOwner {}

    modifier nonReentrant() {
        if (_reentrancyStatus == _ENTERED) revert ReentrantCall();
        _reentrancyStatus = _ENTERED;
        _;
        _reentrancyStatus = _NOT_ENTERED;
    }

    function setExecutor(address executor_) external onlyOwner {
        if (executor_ == address(0)) revert InvalidAddress();
        executor = executor_;
        emit ExecutorSet(executor_);
    }

    function setSettlementOperator(address operator) external onlyOwner {
        if (operator == address(0)) revert InvalidAddress();
        settlementOperator = operator;
        emit SettlementOperatorSet(operator);
    }

    function setApprovedIssuer(address maker, bool approved) external onlyOwner {
        if (maker == address(0)) revert InvalidAddress();
        approvedIssuers[maker] = approved;
        emit IssuerApproval(maker, approved);
    }

    function setMintageCap(address wrappedToken, address maker, uint256 cap) external onlyOwner {
        if (wrappedToken == address(0) || maker == address(0)) revert InvalidAddress();
        _setMintageCap(wrappedToken, maker, cap);
    }

    function setOwnMintageCap(address wrappedToken, uint256 cap) external {
        if (wrappedToken == address(0)) revert InvalidAddress();
        _setMintageCap(wrappedToken, msg.sender, cap);
    }

    function mintageCap(address wrappedToken, address maker) public view returns (uint256) {
        uint256 storedCap = _mintageCap[wrappedToken][maker];
        if (storedCap == 0) return type(uint256).max;
        return storedCap - 1;
    }

    function setSettlementWindow(uint256 newWindow) external onlyOwner {
        _setSettlementWindow(newWindow);
    }

    function setGlobalReclaimWindow(uint256 window) external onlyOwner {
        _setGlobalReclaimWindow(window);
    }

    function computeOrderId(bytes32 quoteHash) public pure returns (bytes32) {
        return keccak256(abi.encode(WRAPPED_ORDER_ID_TYPEHASH, quoteHash));
    }

    function isOrderUsed(bytes32 orderId) external view returns (bool) {
        return mintRecords[orderId].maker != address(0);
    }

    /// @notice Number of live (unsettled, unreclaimed) orders for `maker`.
    function openOrderCount(address maker) external view returns (uint256) {
        return _makerOpenOrders[maker].length();
    }

    /// @notice All live order ids for `maker`. Convenient for an off-chain
    ///         `eth_call`; for very large sets prefer the paginated `openOrders`.
    function openOrderIds(address maker) external view returns (bytes32[] memory) {
        return _makerOpenOrders[maker].values();
    }

    /// @notice Paginated live orders for `maker`, returning each id alongside its
    ///         record so a settlement bot reads deadlines and amounts in one call.
    /// @param start Index into the maker's live-order set.
    /// @param count Maximum entries to return; truncated at the set length.
    function openOrders(address maker, uint256 start, uint256 count)
        external
        view
        returns (bytes32[] memory orderIds, MintRecord[] memory records)
    {
        EnumerableSet.Bytes32Set storage set = _makerOpenOrders[maker];
        uint256 total = set.length();
        if (start >= total || count == 0) {
            return (new bytes32[](0), new MintRecord[](0));
        }
        uint256 end = start + count;
        if (end > total) end = total;
        uint256 n = end - start;
        orderIds = new bytes32[](n);
        records = new MintRecord[](n);
        for (uint256 i = 0; i < n; ++i) {
            bytes32 id = set.at(start + i);
            orderIds[i] = id;
            records[i] = mintRecords[id];
        }
    }

    function mintWrappedTokens(
        bytes32 quoteHash,
        address maker,
        address taker,
        address underlying,
        address takerSellToken,
        uint256 wrappedTokenAmount,
        uint256 lockedSellAmount,
        address recipient
    ) external nonReentrant returns (uint256 mintedAmount) {
        if (msg.sender != executor) revert OnlyExecutor();
        if (
            maker == address(0) || taker == address(0) || underlying == address(0) || takerSellToken == address(0)
                || recipient != settlement
        ) revert InvalidAddress();
        if (wrappedTokenAmount == 0 || lockedSellAmount == 0) revert InvalidAmount();
        if (!approvedIssuers[maker]) revert IssuerNotApproved(maker);

        address wrappedToken = factory.predictWrappedToken(underlying);
        bytes32 orderId = computeOrderId(quoteHash);
        if (mintRecords[orderId].maker != address(0)) revert OrderAlreadyUsed(orderId);

        if (wrappedToken.code.length == 0) {
            wrappedToken = factory.createWrappedToken(underlying);
        }

        uint256 outstanding = outstandingWrapped[wrappedToken][maker];
        uint256 cap = mintageCap(wrappedToken, maker);
        if (outstanding > cap || wrappedTokenAmount > cap - outstanding) {
            revert MintageCapExceeded(outstanding, wrappedTokenAmount, cap);
        }
        outstandingWrapped[wrappedToken][maker] = outstanding + wrappedTokenAmount;

        _pullExact(IERC20(takerSellToken), msg.sender, lockedSellAmount, address(this));

        uint256 settlementDeadline = _mintSettlementDeadline();
        mintRecords[orderId] = MintRecord({
            maker: maker,
            taker: taker,
            underlying: underlying,
            takerSellToken: takerSellToken,
            wrappedTokenAmount: wrappedTokenAmount,
            lockedSellAmount: lockedSellAmount,
            settlementDeadline: settlementDeadline,
            settled: false
        });
        _makerOpenOrders[maker].add(orderId);

        WrappedToken(wrappedToken).authorizeDelivery(taker, wrappedTokenAmount);
        WrappedToken(wrappedToken).mint(recipient, wrappedTokenAmount);

        emit WrappedTokenMinted(
            orderId,
            maker,
            taker,
            underlying,
            wrappedToken,
            takerSellToken,
            wrappedTokenAmount,
            lockedSellAmount,
            settlementDeadline
        );

        return wrappedTokenAmount;
    }

    /// @notice Settle a wrapped fill on behalf of the maker. The settlement operator
    ///         (the Arcus aggregator wallet) or recorded maker may call this. The maker authorizes
    ///         delivery of the real underlying off-chain by signing a Permit2
    ///         `PermitWitnessTransferFrom` over the underlying with a
    ///         `WrappedSettlementAuthorization` witness; the caller forwards it here. The escrow
    ///         pulls the underlying from the maker through Permit2, burns the
    ///         taker's wrapped tokens, delivers the underlying to the taker, and releases the
    ///         locked sell token to the maker.
    /// @param orderId The mint record to settle.
    /// @param permitNonce The Permit2 nonce the maker signed for this settlement transfer.
    /// @param permitDeadline The Permit2 deadline the maker signed (also bound into the witness).
    /// @param makerSig The maker's Permit2 signature over the settlement authorization.
    function settleWrappedToken(bytes32 orderId, uint256 permitNonce, uint256 permitDeadline, bytes calldata makerSig)
        external
        nonReentrant
    {
        MintRecord memory record = _loadLiveRecord(orderId);
        if (msg.sender != settlementOperator && msg.sender != record.maker) revert OnlyOperator();

        address wrappedToken = factory.predictWrappedToken(record.underlying);
        mintRecords[orderId].settled = true;
        _makerOpenOrders[record.maker].remove(orderId);
        outstandingWrapped[wrappedToken][record.maker] -= record.wrappedTokenAmount;

        _pullUnderlyingViaPermit2(orderId, record, permitNonce, permitDeadline, makerSig);
        WrappedToken(wrappedToken).burn(record.taker, record.wrappedTokenAmount);
        _transferExact(IERC20(record.underlying), record.taker, record.wrappedTokenAmount);
        _transferExact(IERC20(record.takerSellToken), record.maker, record.lockedSellAmount);

        emit WrappedTokenSettled(
            orderId,
            record.maker,
            record.taker,
            msg.sender,
            record.takerSellToken,
            record.wrappedTokenAmount,
            record.lockedSellAmount
        );
    }

    /// @dev Pulls `record.wrappedTokenAmount` of the underlying from the maker via Permit2,
    ///      binding the maker's signature to this exact order through the witness. The escrow is
    ///      the Permit2 `spender`, so the maker's signature can only be redeemed here. Reverts on
    ///      any balance shortfall (e.g. fee-on-transfer underlyings).
    function _pullUnderlyingViaPermit2(
        bytes32 orderId,
        MintRecord memory record,
        uint256 permitNonce,
        uint256 permitDeadline,
        bytes calldata makerSig
    ) internal {
        bytes32 witness = keccak256(
            abi.encode(
                WRAPPED_SETTLEMENT_AUTHORIZATION_TYPEHASH,
                orderId,
                record.maker,
                record.underlying,
                record.wrappedTokenAmount,
                permitDeadline
            )
        );

        uint256 beforeBalance = IERC20(record.underlying).balanceOf(address(this));
        PERMIT2.permitWitnessTransferFrom(
            IPermit2.PermitTransferFrom({
                permitted: IPermit2.TokenPermissions({token: record.underlying, amount: record.wrappedTokenAmount}),
                nonce: permitNonce,
                deadline: permitDeadline
            }),
            IPermit2.SignatureTransferDetails({to: address(this), requestedAmount: record.wrappedTokenAmount}),
            record.maker,
            witness,
            WRAPPED_SETTLEMENT_WITNESS_TYPE_STRING,
            makerSig
        );
        uint256 delta = IERC20(record.underlying).balanceOf(address(this)) - beforeBalance;
        if (delta != record.wrappedTokenAmount) {
            revert BalanceDeltaMismatch(record.underlying, record.wrappedTokenAmount, delta);
        }
    }

    function reclaimSellToken(bytes32 orderId) external nonReentrant {
        MintRecord memory record = _loadLiveRecord(orderId);
        if (msg.sender != record.taker && msg.sender != settlementOperator) revert OnlyTakerOrOperator();
        if (block.timestamp <= record.settlementDeadline) revert DeadlineNotReached(record.settlementDeadline);

        address wrappedToken = factory.predictWrappedToken(record.underlying);
        mintRecords[orderId].settled = true;
        _makerOpenOrders[record.maker].remove(orderId);
        outstandingWrapped[wrappedToken][record.maker] -= record.wrappedTokenAmount;

        WrappedToken(wrappedToken).burn(record.taker, record.wrappedTokenAmount);
        _transferExact(IERC20(record.takerSellToken), record.taker, record.lockedSellAmount);

        emit WrappedTokenReclaimed(
            orderId,
            record.maker,
            record.taker,
            msg.sender,
            record.takerSellToken,
            record.wrappedTokenAmount,
            record.lockedSellAmount
        );
    }

    function _loadLiveRecord(bytes32 orderId) internal view returns (MintRecord memory record) {
        record = mintRecords[orderId];
        if (record.maker == address(0)) revert UnknownOrder(orderId);
        if (record.settled) revert OrderTerminated(orderId);
    }

    function _setMintageCap(address wrappedToken, address maker, uint256 cap) internal {
        _mintageCap[wrappedToken][maker] = cap == type(uint256).max ? 0 : cap + 1;
        emit MintageCapSet(wrappedToken, maker, msg.sender, cap);
    }

    function _setSettlementWindow(uint256 newWindow) internal {
        if (newWindow < MIN_WINDOW || newWindow > MAX_WINDOW) revert InvalidWindow();
        settlementWindow = newWindow;
        emit SettlementWindowSet(newWindow);
    }

    function _setGlobalReclaimWindow(uint256 window) internal {
        if (window != 0 && (window < MIN_WINDOW || window > MAX_WINDOW)) revert InvalidWindow();
        globalReclaimWindow = window;
        emit GlobalReclaimWindowSet(window);
    }

    function _mintSettlementDeadline() internal view returns (uint256) {
        uint256 window = globalReclaimWindow;
        if (window == 0) window = settlementWindow;
        return block.timestamp + window;
    }

    function _pullExact(IERC20 token, address from, uint256 amount, address recipient) internal {
        uint256 beforeBalance = token.balanceOf(recipient);
        token.safeTransferFrom(from, recipient, amount);
        uint256 delta = token.balanceOf(recipient) - beforeBalance;
        if (delta != amount) revert BalanceDeltaMismatch(address(token), amount, delta);
    }

    function _transferExact(IERC20 token, address recipient, uint256 amount) internal {
        uint256 beforeBalance = token.balanceOf(recipient);
        token.safeTransfer(recipient, amount);
        uint256 delta = token.balanceOf(recipient) - beforeBalance;
        if (delta != amount) revert BalanceDeltaMismatch(address(token), amount, delta);
    }
}