Institutional Trading on Perpetual DEXs: Block Trading and RFQ

Build institutional-grade features for perpetual DEXs. Implement block trading, RFQ systems, sub-accounts, and compliance tools for professional traders.

Kevan Shah · · 16 min read

Institutional traders bring significant volume but require features beyond standard retail interfaces: block trading for large orders, request-for-quote (RFQ) systems for price certainty, sub-account management for funds, and compliance tools for regulatory requirements.

This guide explores building institutional-grade features for perpetual DEXs to attract professional traders and funds.

Request-for-Quote (RFQ) System

RFQ Mechanism

contract RFQSystem {
    struct RFQRequest {
        bytes32 id;
        address trader;
        address market;
        bool isBuy;
        uint256 size;
        uint256[] acceptablePrices;
        uint256 expiry;
        RFQStatus status;
    }
    
    struct RFQResponse {
        bytes32 rfqId;
        address marketMaker;
        uint256 price;
        uint256 validUntil;
        bool isAccepted;
    }
    
    enum RFQStatus { PENDING, RESPONDED, EXECUTED, EXPIRED }
    
    mapping(bytes32 => RFQRequest) public rfqRequests;
    mapping(bytes32 => RFQResponse[]) public rfqResponses;
    
    uint256 public constant RFQ_VALIDITY = 10 minutes;
    uint256 public constant MIN_RFQ_SIZE = 100000e18; // $100k
    
    event RFQSubmitted(bytes32 indexed rfqId, address indexed trader, uint256 size);
    event RFQResponded(bytes32 indexed rfqId, address indexed mm, uint256 price);
    event RFQExecuted(bytes32 indexed rfqId, address indexed mm, uint256 price);
    
    function submitRFQ(
        address market,
        bool isBuy,
        uint256 size,
        uint256[] calldata acceptablePrices
    ) external returns (bytes32 rfqId) {
        require(size >= MIN_RFQ_SIZE, "Below minimum RFQ size");
        
        rfqId = keccak256(abi.encodePacked(
            msg.sender,
            market,
            block.timestamp
        ));
        
        rfqRequests[rfqId] = RFQRequest({
            id: rfqId,
            trader: msg.sender,
            market: market,
            isBuy: isBuy,
            size: size,
            acceptablePrices: acceptablePrices,
            expiry: block.timestamp + RFQ_VALIDITY,
            status: RFQStatus.PENDING
        });
        
        // Notify market makers
        _notifyMarketMakers(rfqId);
        
        emit RFQSubmitted(rfqId, msg.sender, size);
    }
    
    function respondToRFQ(
        bytes32 rfqId,
        uint256 price
    ) external onlyApprovedMM {
        RFQRequest storage request = rfqRequests[rfqId];
        require(request.status == RFQStatus.PENDING, "RFQ not pending");
        require(block.timestamp < request.expiry, "RFQ expired");
        
        // Validate price is within acceptable range
        bool isAcceptable = false;
        for (uint i = 0; i < request.acceptablePrices.length; i++) {
            if (price <= request.acceptablePrices[i] * 101 / 100 && // 1% tolerance
                price >= request.acceptablePrices[i] * 99 / 100) {
                isAcceptable = true;
                break;
            }
        }
        require(isAcceptable, "Price not acceptable");
        
        rfqResponses[rfqId].push(RFQResponse({
            rfqId: rfqId,
            marketMaker: msg.sender,
            price: price,
            validUntil: block.timestamp + 2 minutes,
            isAccepted: false
        }));
        
        request.status = RFQStatus.RESPONDED;
        
        emit RFQResponded(rfqId, msg.sender, price);
    }
    
    function acceptRFQResponse(
        bytes32 rfqId,
        uint256 responseIndex
    ) external {
        RFQRequest storage request = rfqRequests[rfqId];
        require(request.trader == msg.sender, "Not RFQ owner");
        require(request.status == RFQStatus.RESPONDED, "Invalid status");
        
        RFQResponse storage response = rfqResponses[rfqId][responseIndex];
        require(!response.isAccepted, "Already accepted");
        require(block.timestamp < response.validUntil, "Response expired");
        
        response.isAccepted = true;
        request.status = RFQStatus.EXECUTED;
        
        // Execute trade at agreed price
        _executeRFQTrade(request, response);
        
        emit RFQExecuted(rfqId, response.marketMaker, response.price);
    }
    
    function _executeRFQTrade(
        RFQRequest memory request,
        RFQResponse memory response
    ) internal {
        // Execute off-chain or via specialized contract
        // Handle large size with minimal slippage
        
        if (request.isBuy) {
            // Trader buys at response.price
            _openPosition(request.trader, request.market, true, request.size, response.price);
            _openPosition(response.marketMaker, request.market, false, request.size, response.price);
        } else {
            // Trader sells at response.price
            _openPosition(request.trader, request.market, false, request.size, response.price);
            _openPosition(response.marketMaker, request.market, true, request.size, response.price);
        }
    }
}

Block Trading System

contract BlockTrading {
    struct BlockTrade {
        bytes32 id;
        address initiator;
        address counterparty;
        address market;
        bool isBuy;
        uint256 size;
        uint256 price;
        uint256 timestamp;
        bool settled;
    }
    
    mapping(bytes32 => BlockTrade) public blockTrades;
    
    uint256 public constant BLOCK_MIN_SIZE = 500000e18; // $500k
    uint256 public constant BLOCK_DISCOUNT = 50; // 0.5% discount on fees
    
    event BlockTradeInitiated(
        bytes32 indexed tradeId,
        address indexed initiator,
        uint256 size
    );
    
    event BlockTradeSettled(bytes32 indexed tradeId, uint256 price);
    
    function initiateBlockTrade(
        address market,
        bool isBuy,
        uint256 size,
        uint256 targetPrice
    ) external onlyInstitutional returns (bytes32 tradeId) {
        require(size >= BLOCK_MIN_SIZE, "Below block minimum");
        
        tradeId = keccak256(abi.encodePacked(
            msg.sender,
            market,
            block.timestamp
        ));
        
        blockTrades[tradeId] = BlockTrade({
            id: tradeId,
            initiator: msg.sender,
            counterparty: address(0),
            market: market,
            isBuy: isBuy,
            size: size,
            price: targetPrice,
            timestamp: block.timestamp,
            settled: false
        });
        
        // Match with block liquidity pool or institutional counterparty
        _matchBlockTrade(tradeId);
        
        emit BlockTradeInitiated(tradeId, msg.sender, size);
    }
    
    function provideBlockLiquidity(
        address market,
        uint256 maxSize,
        uint256 minPremium
    ) external onlyApprovedMM {
        // Market makers can provide liquidity specifically for block trades
        blockLiquidityProviders.push(BlockLiquidityProvider({
            mm: msg.sender,
            market: market,
            maxSize: maxSize,
            minPremium: minPremium,
            available: true
        }));
    }
    
    function _matchBlockTrade(bytes32 tradeId) internal {
        BlockTrade storage trade = blockTrades[tradeId];
        
        // Find best counterparty from block liquidity providers
        address bestCounterparty;
        uint256 bestPrice = trade.isBuy ? type(uint256).max : 0;
        
        for (uint i = 0; i < blockLiquidityProviders.length; i++) {
            BlockLiquidityProvider memory provider = blockLiquidityProviders[i];
            
            if (provider.market != trade.market) continue;
            if (provider.maxSize < trade.size) continue;
            if (!provider.available) continue;
            
            // Calculate price with premium
            uint256 oraclePrice = oracle.getPrice(trade.market);
            uint256 offeredPrice = trade.isBuy
                ? oraclePrice + (oraclePrice * provider.minPremium / 10000)
                : oraclePrice - (oraclePrice * provider.minPremium / 10000);
            
            if (trade.isBuy && offeredPrice < bestPrice) {
                bestPrice = offeredPrice;
                bestCounterparty = provider.mm;
            } else if (!trade.isBuy && offeredPrice > bestPrice) {
                bestPrice = offeredPrice;
                bestCounterparty = provider.mm;
            }
        }
        
        require(bestCounterparty != address(0), "No block liquidity available");
        
        trade.counterparty = bestCounterparty;
        trade.price = bestPrice;
        
        // Execute with reduced fees
        _executeBlockTrade(tradeId, BLOCK_DISCOUNT);
    }
}

Sub-Account Management

contract SubAccountManager {
    struct SubAccount {
        address owner;
        string name;
        uint256 margin;
        mapping(bytes32 => Position) positions;
        bool isActive;
    }
    
    mapping(address => mapping(uint256 => SubAccount)) public subAccounts;
    mapping(address => uint256) public subAccountCount;
    
    uint256 public constant MAX_SUB_ACCOUNTS = 20;
    
    event SubAccountCreated(address indexed owner, uint256 indexed subAccountId, string name);
    
    function createSubAccount(string calldata name) external returns (uint256 subAccountId) {
        require(bytes(name).length > 0, "Empty name");
        
        subAccountId = subAccountCount[msg.sender];
        require(subAccountId < MAX_SUB_ACCOUNTS, "Max sub-accounts reached");
        
        subAccounts[msg.sender][subAccountId] = SubAccount({
            owner: msg.sender,
            name: name,
            margin: 0,
            isActive: true
        });
        
        subAccountCount[msg.sender]++;
        
        emit SubAccountCreated(msg.sender, subAccountId, name);
    }
    
    function depositToSubAccount(uint256 subAccountId, uint256 amount) external {
        SubAccount storage sub = subAccounts[msg.sender][subAccountId];
        require(sub.isActive, "Sub-account not active");
        
        collateralToken.transferFrom(msg.sender, address(this), amount);
        sub.margin += amount;
    }
    
    function openPositionInSubAccount(
        uint256 subAccountId,
        address market,
        bool isLong,
        uint256 size
    ) external {
        SubAccount storage sub = subAccounts[msg.sender][subAccountId];
        require(sub.isActive, "Sub-account not active");
        require(sub.owner == msg.sender, "Not owner");
        
        uint256 requiredMargin = calculateMargin(market, size);
        require(sub.margin >= requiredMargin, "Insufficient margin");
        
        // Open position within sub-account
        bytes32 positionId = _openPosition(msg.sender, market, isLong, size);
        
        sub.positions[positionId] = Position({
            positionId: positionId,
            size: size,
            isLong: isLong,
            entryPrice: oracle.getPrice(market),
            timestamp: block.timestamp
        });
        
        sub.margin -= requiredMargin;
    }
    
    function getSubAccountSummary(address trader, uint256 subAccountId)
        external
        view
        returns (
            string memory name,
            uint256 margin,
            uint256 allocatedMargin,
            int256 unrealizedPnL,
            uint256 positionCount
        )
    {
        SubAccount storage sub = subAccounts[trader][subAccountId];
        
        name = sub.name;
        margin = sub.margin;
        
        // Calculate allocated margin and PnL
        (allocatedMargin, unrealizedPnL, positionCount) = 
            calculateSubAccountMetrics(sub);
    }
    
    function transferBetweenSubAccounts(
        uint256 fromSubAccount,
        uint256 toSubAccount,
        uint256 amount
    ) external {
        SubAccount storage fromSub = subAccounts[msg.sender][fromSubAccount];
        SubAccount storage toSub = subAccounts[msg.sender][toSubAccount];
        
        require(fromSub.isActive && toSub.isActive, "Invalid sub-account");
        require(fromSub.margin >= amount, "Insufficient margin");
        
        fromSub.margin -= amount;
        toSub.margin += amount;
    }
}

Compliance and Reporting

contract InstitutionalCompliance {
    struct KYCInfo {
        bool isVerified;
        uint256 verificationLevel; // 1=basic, 2=advanced, 3=institutional
        bytes32 kycHash;
        uint256 verificationExpiry;
        string jurisdiction;
    }
    
    mapping(address => KYCInfo) public kycData;
    mapping(string => bool) public restrictedJurisdictions;
    
    struct TradeReport {
        bytes32 tradeId;
        address trader;
        address market;
        bool isBuy;
        uint256 size;
        uint256 price;
        uint256 timestamp;
        bytes32 counterparty;
    }
    
    TradeReport[] public tradeHistory;
    
    event KYCVerified(address indexed trader, uint256 level);
    event TradeReported(bytes32 indexed tradeId, address indexed trader);
    
    function verifyKYC(
        address trader,
        uint256 level,
        bytes32 kycHash,
        string calldata jurisdiction
    ) external onlyKYCProvider {
        require(!restrictedJurisdictions[jurisdiction], "Restricted jurisdiction");
        
        kycData[trader] = KYCInfo({
            isVerified: true,
            verificationLevel: level,
            kycHash: kycHash,
            verificationExpiry: block.timestamp + 365 days,
            jurisdiction: jurisdiction
        });
        
        emit KYCVerified(trader, level);
    }
    
    function reportTrade(
        bytes32 tradeId,
        address trader,
        address market,
        bool isBuy,
        uint256 size,
        uint256 price,
        bytes32 counterparty
    ) external {
        require(kycData[trader].isVerified, "Trader not KYC'd");
        
        tradeHistory.push(TradeReport({
            tradeId: tradeId,
            trader: trader,
            market: market,
            isBuy: isBuy,
            size: size,
            price: price,
            timestamp: block.timestamp,
            counterparty: counterparty
        }));
        
        emit TradeReported(tradeId, trader);
    }
    
    function generateComplianceReport(
        address trader,
        uint256 startTime,
        uint256 endTime
    ) external view returns (bytes memory) {
        require(
            msg.sender == trader || msg.sender == complianceOfficer,
            "Unauthorized"
        );
        
        // Generate report of all trades in timeframe
        TradeReport[] memory relevantTrades = new TradeReport[](tradeHistory.length);
        uint256 count = 0;
        
        for (uint i = 0; i < tradeHistory.length; i++) {
            if (tradeHistory[i].trader == trader &&
                tradeHistory[i].timestamp >= startTime &&
                tradeHistory[i].timestamp <= endTime) {
                relevantTrades[count] = tradeHistory[i];
                count++;
            }
        }
        
        return formatComplianceReport(relevantTrades, count);
    }
    
    function checkTradingLimits(
        address trader,
        uint256 tradeSize
    ) external view returns (bool, string memory) {
        KYCInfo memory kyc = kycData[trader];
        
        if (!kyc.isVerified) {
            return (false, "KYC required");
        }
        
        if (block.timestamp > kyc.verificationExpiry) {
            return (false, "KYC expired");
        }
        
        // Check daily limits based on KYC level
        uint256 dailyVolume = getDailyVolume(trader);
        uint256 limit = getDailyLimit(kyc.verificationLevel);
        
        if (dailyVolume + tradeSize > limit) {
            return (false, "Daily limit exceeded");
        }
        
        return (true, "");
    }
}

API and Connectivity

contract TradingAPI {
    mapping(address => bytes32) public apiKeys;
    mapping(bytes32 => uint256) public rateLimits;
    mapping(bytes32 => uint256) public lastRequestTime;
    
    uint256 public constant RATE_LIMIT_PER_MINUTE = 100;
    
    event APIKeyGenerated(address indexed trader, bytes32 apiKey);
    
    function generateAPIKey() external onlyVerified returns (bytes32 apiKey) {
        apiKey = keccak256(abi.encodePacked(msg.sender, block.timestamp, nonce++));
        apiKeys[msg.sender] = apiKey;
        rateLimits[apiKey] = RATE_LIMIT_PER_MINUTE;
        
        emit APIKeyGenerated(msg.sender, apiKey);
    }
    
    modifier validAPIKey(bytes32 apiKey) {
        require(apiKeys[msg.sender] == apiKey, "Invalid API key");
        require(checkRateLimit(apiKey), "Rate limit exceeded");
        _;
    }
    
    function checkRateLimit(bytes32 apiKey) internal returns (bool) {
        if (block.timestamp > lastRequestTime[apiKey] + 1 minutes) {
            lastRequestTime[apiKey] = block.timestamp;
            return true;
        }
        
        // Check within current minute
        return true; // Simplified
    }
    
    function apiPlaceOrder(
        bytes32 apiKey,
        address market,
        bool isBuy,
        uint256 size,
        uint256 price,
        bool isLimit
    ) external validAPIKey(apiKey) {
        if (isLimit) {
            placeLimitOrder(market, isBuy, price, size);
        } else {
            placeMarketOrder(market, isBuy, size);
        }
    }
    
    function apiCancelOrder(
        bytes32 apiKey,
        bytes32 orderId
    ) external validAPIKey(apiKey) {
        cancelOrder(orderId);
    }
    
    function apiGetPosition(
        bytes32 apiKey
    ) external view validAPIKey(apiKey) returns (bytes memory) {
        return formatPositionData(msg.sender);
    }
}

Best Practices

1. Tiered Access

  • Different KYC levels for different features
  • Higher limits for verified institutions
  • Dedicated support channels

2. Privacy Protection

  • Anonymous trading options where legal
  • Encrypted communication
  • Secure key management

3. Regulatory Compliance

  • Real-time monitoring
  • Automated reporting
  • Audit trails

4. Professional Tools

  • FIX API support
  • WebSocket feeds
  • Advanced order types

5. Risk Management

  • Position limits
  • Credit checks
  • Real-time monitoring

Conclusion

Attracting institutional traders requires:

  1. Sophisticated Order Types: RFQ, block trading, TWAP
  2. Account Management: Sub-accounts, master-trader relationships
  3. Compliance Tools: KYC, reporting, monitoring
  4. Professional APIs: Low-latency, high-throughput
  5. Support Services: Dedicated relationship managers

The perpetual DEXs that capture institutional volume will offer the reliability, compliance, and sophistication of traditional exchanges—with the transparency and efficiency of DeFi.