Contract detail
SecondaryMarketEscrow
Live Robinhood Chain account facts for 0x7e318ef3…cefeb5df with at least 5 observed transactions. The history total is updating as indexing continues.
- Address type
- Smart contract
- Transactions
- 5+ observed
- First indexed block
- #203512
- Latest indexed block
- #88038970
- History coverage
- Recent activity available; total is catching up
- ERC-20 transfers
- 1
- Emitted events
- 59
- Verification
- Verified
- Compiler
- v0.8.30+commit.73712a01
- Verification match
- full
- Creation transaction
- 0x094d7396…38115b2e
- Created at block
- #203512
- Language
- solidity
- EVM version
- prague
- License
- none
- Compiler optimization
- Enabled · 200 runs
Related activity
Transactions
Verified contract
Contract source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/interfaces/IERC1271.sol";
import "./interfaces/ISecondaryMarketEscrow.sol";
import "./utils/ECDSAHelper.sol";
/**
* @title SecondaryMarketEscrow
* @notice Permissionless atomic OTC swap for V2 position tokens.
* @dev No ownership, no funds at rest. Both parties sign off-chain via EIP-712
* and anyone can submit the trade. Signatures are accepted from EOAs
* (ECDSA) and from smart accounts via ERC-1271 (`isValidSignature`). The
* legacy `sessionKeyData` blob path was removed because its factory
* CREATE2 derivation could not see Kernel validator rotations; smart
* accounts using session keys must now sign through the account itself.
* Supplying any non-empty `sessionKeyData` reverts with
* `LegacySessionKeyDataDisabled`.
*
* The session-key revocation registry below is preserved as an advisory,
* caller-scoped on-chain log for off-chain tooling. The contract no
* longer reads it during signature validation.
*/
contract SecondaryMarketEscrow is
ISecondaryMarketEscrow,
EIP712("SecondaryMarketEscrow", "1"),
ReentrancyGuard
{
using SafeERC20 for IERC20;
// ============ Constants ============
/// @notice EIP-712 typehash for trade approval
bytes32 public constant TRADE_APPROVAL_TYPEHASH = keccak256(
"TradeApproval(bytes32 tradeHash,address signer,uint256 nonce,uint256 deadline)"
);
/// @notice Gas limit for EIP-1271 signature validation calls
uint256 internal constant EIP1271_GAS_LIMIT = 500_000;
// ============ State ============
/// @notice Bitmap nonces for replay protection (Permit2-style)
mapping(address => mapping(uint256 => uint256)) private _nonceBitmap;
/// @notice Advisory revocation registry: caller => sessionKey =>
/// revokedAt timestamp. Not consulted by signature validation.
mapping(address => mapping(address => uint256)) private _revokedSessionKeys;
// ============ Session Key Management ============
/// @inheritdoc ISecondaryMarketEscrow
function revokeSessionKey(address sessionKey) external {
_revokedSessionKeys[msg.sender][sessionKey] = block.timestamp;
emit SessionKeyRevoked(msg.sender, sessionKey, block.timestamp);
}
/// @inheritdoc ISecondaryMarketEscrow
function isSessionKeyRevoked(address caller, address sessionKey)
external
view
returns (bool revoked)
{
return _revokedSessionKeys[caller][sessionKey] > 0;
}
// ============ External Functions ============
/// @inheritdoc ISecondaryMarketEscrow
function executeTrade(TradeRequest calldata request) external nonReentrant {
// Validate basic parameters
if (request.tokenAmount == 0 || request.price == 0) {
revert ZeroAmount();
}
if (request.seller == request.buyer) {
revert SellerBuyerSame();
}
// Compute trade hash
bytes32 tradeHash = keccak256(
abi.encode(
request.token,
request.collateral,
request.seller,
request.buyer,
request.tokenAmount,
request.price
)
);
// Validate seller signature
if (!_validatePartySignature(
tradeHash,
request.seller,
request.sellerNonce,
request.sellerDeadline,
request.sellerSignature,
request.sellerSessionKeyData
)) {
revert InvalidSignature();
}
// Validate buyer signature
if (!_validatePartySignature(
tradeHash,
request.buyer,
request.buyerNonce,
request.buyerDeadline,
request.buyerSignature,
request.buyerSessionKeyData
)) {
revert InvalidSignature();
}
// Use bitmap nonces (reverts if already used)
_useNonce(request.seller, request.sellerNonce);
_useNonce(request.buyer, request.buyerNonce);
// Execute atomic swap
// 1. Transfer position tokens from seller to buyer
IERC20(request.token)
.safeTransferFrom(
request.seller, request.buyer, request.tokenAmount
);
// 2. Transfer collateral from buyer to seller
IERC20(request.collateral)
.safeTransferFrom(request.buyer, request.seller, request.price);
emit TradeExecuted(
tradeHash,
request.seller,
request.buyer,
request.token,
request.collateral,
request.tokenAmount,
request.price,
request.refCode
);
}
// ============ View Functions ============
/// @inheritdoc ISecondaryMarketEscrow
function isNonceUsed(address account, uint256 nonce)
external
view
returns (bool used)
{
uint256 wordPos = nonce >> 8;
uint256 bitPos = nonce & 0xff;
return (_nonceBitmap[account][wordPos] & (1 << bitPos)) != 0;
}
/// @inheritdoc ISecondaryMarketEscrow
function nonceBitmap(address account, uint256 wordPos)
external
view
returns (uint256 word)
{
return _nonceBitmap[account][wordPos];
}
/// @inheritdoc ISecondaryMarketEscrow
function domainSeparator() external view returns (bytes32 separator) {
return _domainSeparatorV4();
}
/// @inheritdoc ISecondaryMarketEscrow
function getTradeHash(
address token,
address collateral,
address seller,
address buyer,
uint256 tokenAmount,
uint256 price
) public pure returns (bytes32 tradeHash) {
return keccak256(
abi.encode(token, collateral, seller, buyer, tokenAmount, price)
);
}
/// @inheritdoc ISecondaryMarketEscrow
function getTradeApprovalHash(
bytes32 tradeHash,
address signer,
uint256 nonce,
uint256 deadline
) public view returns (bytes32 hash) {
bytes32 structHash = keccak256(
abi.encode(
TRADE_APPROVAL_TYPEHASH, tradeHash, signer, nonce, deadline
)
);
return _hashTypedDataV4(structHash);
}
// ============ Internal: Nonce Management ============
/// @notice Mark a nonce as used (Permit2-style bitmap)
/// @param account The account whose nonce to use
/// @param nonce The nonce value to consume
function _useNonce(address account, uint256 nonce) internal {
uint256 wordPos = nonce >> 8;
uint256 bitPos = nonce & 0xff;
uint256 bit = 1 << bitPos;
uint256 word = _nonceBitmap[account][wordPos];
if (word & bit != 0) revert NonceAlreadyUsed();
_nonceBitmap[account][wordPos] = word | bit;
}
// ============ Internal: Signature Validation ============
/// @notice Validate a party's signature.
/// @dev Accepts an EOA ECDSA signature or a smart-account ERC-1271
/// signature. The legacy `sessionKeyData` blob path is no longer supported
/// — supplying a non-empty blob reverts with `LegacySessionKeyDataDisabled`.
/// Smart accounts using session keys must have the session key authorized
/// via the smart account's own validator (e.g. Kernel permission validator)
/// so `isValidSignature` returns the magic value.
function _validatePartySignature(
bytes32 tradeHash,
address signer,
uint256 nonce,
uint256 deadline,
bytes calldata signature,
bytes calldata sessionKeyData
) internal view returns (bool isValid) {
if (sessionKeyData.length != 0) {
revert LegacySessionKeyDataDisabled();
}
return _isTradeApprovalValidWithEIP1271Fallback(
tradeHash, signer, nonce, deadline, signature
);
}
/// @notice Validate trade approval via ECDSA
function _isTradeApprovalValid(
bytes32 tradeHash,
address signer,
uint256 nonce,
uint256 deadline,
bytes memory signature
) internal view returns (bool isValid) {
if (block.timestamp > deadline) {
return false;
}
bytes32 hash = getTradeApprovalHash(tradeHash, signer, nonce, deadline);
return ECDSAHelper.isValidECDSASignature(hash, signature, signer);
}
/// @notice Validate trade approval with EIP-1271 fallback for smart contracts
function _isTradeApprovalValidWithEIP1271Fallback(
bytes32 tradeHash,
address signer,
uint256 nonce,
uint256 deadline,
bytes memory signature
) internal view returns (bool isValid) {
if (block.timestamp > deadline) {
return false;
}
// Try ECDSA first (for EOAs)
if (_isTradeApprovalValid(
tradeHash, signer, nonce, deadline, signature
)) {
return true;
}
// Fallback to EIP-1271 for contracts
if (signer.code.length > 0) {
bytes32 hash =
getTradeApprovalHash(tradeHash, signer, nonce, deadline);
return _isEIP1271SignatureValid(signer, hash, signature);
}
return false;
}
/// @notice Validate signature using EIP-1271 (for smart contract signers)
function _isEIP1271SignatureValid(
address signer,
bytes32 hash,
bytes memory signature
) internal view returns (bool isValid) {
if (signer.code.length == 0) {
return false;
}
try IERC1271(signer).isValidSignature{ gas: EIP1271_GAS_LIMIT }(
hash, signature
) returns (
bytes4 magicValue
) {
return magicValue == IERC1271.isValidSignature.selector;
} catch {
return false;
}
}
}