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);
}
}