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.
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:
- Sophisticated Order Types: RFQ, block trading, TWAP
- Account Management: Sub-accounts, master-trader relationships
- Compliance Tools: KYC, reporting, monitoring
- Professional APIs: Low-latency, high-throughput
- 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.