Secondary Market (P2P)
Peer-to-peer marketplace for negotiated trades at custom prices
Overview
The Secondary Market is a peer-to-peer order book where users trade YRT tokens directly with each other at negotiated prices—independent of liquidity pool pricing.
Key Innovation:
Off-chain orders, on-chain settlement - Gas efficient
EIP-712 signatures - Secure, standard order signing
Zero protocol fees - Direct peer-to-peer swaps
No slippage - Exact price matching
Why Two Markets?
Owna Finance provides dual trading venues to maximize liquidity and serve different trading needs:
🏦 Owna-DEX (AMM)
For instant trading at market price
Instant execution
Trade immediately
Price impact on large orders
Always liquid
24/7 availability
0.3% swap fee
Dynamic pricing
Market-driven
Slippage on volatile assets
No counterparty needed
Trade against pool
Must accept pool price
Best for:
Small to medium trades
Immediate execution needs
Users who accept current market price
High-frequency trading
🤝 Secondary Market (Order Book)
For negotiated trading at custom prices
Custom pricing
Set your own price
Must wait for counterparty
Zero fees
No protocol fee
Order may not fill
No slippage
Exact price guaranteed
Requires active matching
Large order friendly
No price impact
Off-chain order management
Best for:
Large block trades
Price-sensitive traders
Limit orders at specific prices
OTC (over-the-counter) deals
Professional Rationale: Dual-Market Approach
🎯 Market Efficiency Through Choice
Problem: Single trading venue forces all users into one mechanism
AMM: Instant but expensive for large trades
Order Book: Efficient pricing but requires liquidity
Solution: Dual markets serve different needs simultaneously
1. Capital Efficiency
Large trades split intelligently:
Example: User wants to buy 10,000 YRT
Option A (DEX only):
- 10,000 YRT via AMM
- Price impact: ~15%
- Fees: 0.3%
- Total cost: High slippage + fees
Option B (Dual Market):
- 5,000 YRT via Secondary Market (negotiated price, 0% fee)
- 5,000 YRT via DEX (lower impact due to smaller size)
- Total cost: Optimized2. Liquidity Concentration
AMM provides base liquidity floor:
Always available for small/medium trades
Price discovery mechanism
24/7 instant execution
Order Book provides deep liquidity for size:
Large institutional orders
Custom pricing for bulk purchases
OTC settlement without pool impact
3. Risk Segregation
AMM Risk:
Impermanent loss for LPs
Slippage for large trades
Front-running vulnerability
Order Book Risk:
Order may not fill
Counterparty required
Off-chain order management
Users choose risk profile per trade.
4. Market Resilience
If AMM liquidity is thin:
Users post Secondary Market orders at fair prices
Orders fill when counterparties appear
Market continues functioning
If Order Book has no takers:
Users execute via AMM instantly
Trading never stops
Liquidity always exists
How It Works
Architecture
Order Lifecycle
1. Maker Creates Order (Off-chain)
SwapOrder memory order = SwapOrder({
maker: sellerAddress,
makerToken: yrtAddress, // Selling YRT
makerAmount: 1000e18, // 1000 YRT
takerToken: usdcAddress, // Want USDC
takerAmount: 1200e18, // At 1.2 USDC/YRT
salt: randomNonce // Unique order ID
});
// Sign with EIP-712
bytes memory signature = signTypedData(order);2. Order Broadcasted (Off-chain)
Order stored in order book database
Visible to all potential takers
No gas cost until filled
3. Taker Fills Order (On-chain)
// Buyer executes trade
secondaryMarket.executeSwap(order, signature);
// Contract verifies:
// 1. Signature valid (maker signed this order)
// 2. Order not already filled/cancelled
// 3. Both parties have sufficient balance
// 4. Both parties approved contract
// Atomic swap:
// - 1000 YRT: Seller → Buyer
// - 1200 USDC: Buyer → Seller4. Order Cancellation (Optional)
// Maker can cancel unfilled order
secondaryMarket.cancelOrder(order);Key Features
🔒 EIP-712 Typed Signatures
Orders signed with EIP-712 standard for security:
bytes32 private constant SWAP_ORDER_TYPEHASH = keccak256(
"SwapOrder(address maker,address makerToken,uint256 makerAmount,address takerToken,uint256 takerAmount,uint256 salt)"
);Benefits:
Human-readable order preview in wallet
Prevents signature replay attacks
Industry-standard security
Compatible with all major wallets
💰 Zero Protocol Fees
Unlike DEX (0.3% fee), Secondary Market has:
0% protocol fee on trades
Only gas costs (same as any token transfer)
Maximizes value for both parties
Comparison:
DEX Swap (100 USDC → YRT):
- Swap fee: 0.30 USDC
- Gas cost: ~0.10 USDC
- Total cost: 0.40 USDC
Secondary Market (100 USDC → YRT):
- Swap fee: 0 USDC
- Gas cost: ~0.15 USDC
- Total cost: 0.15 USDC
Savings: 62.5%🎯 Exact Price Execution
No slippage—trade executes at exact order price:
AMM Slippage Example:
- Expected: 100 YRT at 1.0 USDC/YRT
- Actual: 95 YRT at 1.05 USDC/YRT
- Slippage: 5%
Secondary Market:
- Order: 100 YRT at 1.0 USDC/YRT
- Execution: 100 YRT at 1.0 USDC/YRT
- Slippage: 0%🔄 Order Status Tracking
Three order states:
NONE
Order not yet filled/cancelled
✅ Yes
FILLED
Order successfully executed
❌ No
CANCELLED
Order cancelled by maker
❌ No
Prevents double-spending and replay attacks.
Trade Scenarios
Scenario 1: Bulk Purchase Below Market
Setup:
AMM Price: 1.2 USDC/YRT
Large buyer wants 10,000 YRT
AMM slippage would be ~20% on this size
Secondary Market Solution:
// Buyer posts order: "I'll buy 10,000 YRT at 1.15 USDC/YRT"
SwapOrder({
maker: buyerAddress,
makerToken: usdcAddress,
makerAmount: 11500e18, // 11,500 USDC
takerToken: yrtAddress,
takerAmount: 10000e18, // 10,000 YRT
salt: nonce
});Result:
Buyer gets 10,000 YRT at 1.15 (vs 1.44 via AMM)
Seller gets better price than AMM (1.15 > 1.2 after fees)
Both parties save vs AMM slippage
Zero protocol fees
Scenario 2: OTC Property Sale
Setup:
Investor A holds 50,000 YRT from Property X
Investor B wants entire position
Too large for AMM without massive slippage
Secondary Market Solution:
// Investor A posts sell order
SwapOrder({
maker: investorA,
makerToken: yrtAddress,
makerAmount: 50000e18, // 50,000 YRT
takerToken: usdcAddress,
takerAmount: 55000e18, // 55,000 USDC (1.1 USDC/YRT)
salt: nonce
});Result:
Single atomic transaction
No AMM pool disturbance
Custom negotiated price
Off-chain negotiation, on-chain settlement
Scenario 3: Limit Orders
Setup:
Current AMM price: 1.0 USDC/YRT
Trader believes price will rise to 1.3
Wants to sell at 1.25 when it reaches that level
Secondary Market Solution:
// Trader posts sell limit order
SwapOrder({
maker: traderAddress,
makerToken: yrtAddress,
makerAmount: 1000e18, // 1,000 YRT
takerToken: usdcAddress,
takerAmount: 1250e18, // 1,250 USDC (1.25 USDC/YRT)
salt: nonce
});
// Order sits in book until:
// - AMM price rises to 1.25+ (arbitrageur fills order)
// - Another buyer wants YRT at 1.25
// - Trader cancels orderTechnical Implementation
Contract: SecondaryMarket.sol
Deployed at: [TBD - See technical/addresses.md]
Core Functions
1. executeSwap()
function executeSwap(
SwapOrder calldata _order,
bytes calldata _signature
) external nonReentrantExecutes atomic swap between maker and taker.
Validation:
✅ Signature matches maker address
✅ Order not already filled/cancelled
✅ Valid addresses and amounts
✅ Sufficient balances and approvals
Process:
Hash order with EIP-712
Recover signer from signature
Verify signer == order.maker
Mark order as FILLED
Transfer makerToken: maker → taker
Transfer takerToken: taker → maker
Emit SwapExecuted event
2. cancelOrder()
function cancelOrder(SwapOrder calldata _order) externalCancels unfilled order (maker only).
Validation:
✅ Caller is order maker
✅ Order not already filled/cancelled
3. getOrderStatus()
function getOrderStatus(bytes32 _orderHash)
external view returns (uint256 status)Returns order state: 0 = NONE, 1 = FILLED, 2 = CANCELLED
Security Features
✅ ReentrancyGuard
All state-changing functions protected against reentrancy attacks.
✅ EIP-712 Typed Data
Standard signature format prevents:
Signature reuse across chains/contracts
Phishing attacks (wallet shows clear order details)
Replay attacks from other dApps
✅ SafeERC20
Uses OpenZeppelin SafeERC20 for all token transfers—handles non-standard ERC20 implementations.
✅ Order Hash Uniqueness
Salt parameter ensures order uniqueness:
SwapOrder({
// ... order details ...
salt: block.timestamp + randomNumber
});Different salt = different hash = new order (even with same amounts)
✅ Order Status Tracking
Prevents double-execution:
mapping(bytes32 orderHash => OrderStatus) private s_orderStatus;
if (s_orderStatus[orderHash] == OrderStatus.FILLED) {
revert SecondaryMarket__OrderAlreadyFilled(orderHash);
}Off-Chain Infrastructure
Order Book Management
Not included in smart contract (gas optimization):
Order storage and indexing
Order matching engine
Order broadcasting
Historical trade data
Implementation options:
Centralized Order Book
Fast order matching
Low latency
Requires trusted operator
Example: Custom API backend
Decentralized Order Relay
P2P order broadcasting
No central authority
Higher latency
Example: IPFS + libp2p
Hybrid Approach (Recommended)
Multiple independent relayers
Users choose preferred relayer
All orders settle on same contract
Example: Multiple frontends, shared contract
User Flows
Selling YRT Tokens
// 1. Create order
const order = {
maker: userAddress,
makerToken: yrtAddress,
makerAmount: ethers.parseEther("1000"), // 1000 YRT
takerToken: usdcAddress,
takerAmount: ethers.parseEther("1200"), // Want 1200 USDC
salt: Date.now()
};
// 2. Sign with EIP-712
const domain = {
name: "SecondaryMarket",
version: "1",
chainId: 84532,
verifyingContract: secondaryMarketAddress
};
const types = {
SwapOrder: [
{ name: "maker", type: "address" },
{ name: "makerToken", type: "address" },
{ name: "makerAmount", type: "uint256" },
{ name: "takerToken", type: "address" },
{ name: "takerAmount", type: "uint256" },
{ name: "salt", type: "uint256" }
]
};
const signature = await signer.signTypedData(domain, types, order);
// 3. Approve contract to spend YRT
await yrtToken.approve(secondaryMarketAddress, order.makerAmount);
// 4. Broadcast order to order book (off-chain)
await fetch("/api/orders", {
method: "POST",
body: JSON.stringify({ order, signature })
});
// Order now visible to all potential buyersBuying YRT Tokens
// 1. Browse available orders (from API/UI)
const orders = await fetch("/api/orders?makerToken=YRT").then(r => r.json());
// 2. Select order
const selectedOrder = orders[0]; // Best price
// 3. Approve contract to spend USDC
await usdcToken.approve(
secondaryMarketAddress,
selectedOrder.takerAmount
);
// 4. Execute trade on-chain
const tx = await secondaryMarket.executeSwap(
selectedOrder,
selectedOrder.signature
);
await tx.wait();
console.log("Trade completed! YRT received.");DEX vs Secondary Market Comparison
When to Use DEX (AMM)
✅ Best for:
Small to medium trades (< $10,000)
Immediate execution required
Market orders
High-frequency trading
Don't want to wait for counterparty
❌ Avoid for:
Large block trades (> $50,000)
Price-sensitive transactions
When market is volatile (high slippage)
When to Use Secondary Market
✅ Best for:
Large block trades (> $50,000)
Custom pricing requirements
Limit orders
OTC deals
Minimizing fees
When you can wait for counterparty
❌ Avoid for:
Need instant execution
Small trade sizes (gas cost > savings)
Urgent trades
Integration Example
Smart Order Routing
Route trades to optimal venue:
async function executeTrade(
tokenIn: string,
tokenOut: string,
amountIn: bigint
): Promise<void> {
// 1. Check both venues
const ammQuote = await router.getAmountsOut(amountIn, [tokenIn, tokenOut]);
const orderBookQuote = await findBestOrder(tokenIn, tokenOut, amountIn);
// 2. Compare effective prices
const ammPrice = ammQuote.amountOut / amountIn;
const ammPriceAfterFee = ammPrice * 0.997; // 0.3% fee
const orderBookPrice = orderBookQuote
? orderBookQuote.takerAmount / orderBookQuote.makerAmount
: 0;
// 3. Route to better venue
if (orderBookQuote && orderBookPrice > ammPriceAfterFee) {
console.log("Routing to Secondary Market (better price)");
await secondaryMarket.executeSwap(orderBookQuote, signature);
} else {
console.log("Routing to DEX (instant execution)");
await router.swapExactTokensForTokens(
amountIn,
minAmountOut,
[tokenIn, tokenOut],
userAddress,
deadline
);
}
}Best Practices
For Makers (Order Creators)
1. Competitive Pricing
// Check AMM price first
const ammPrice = await getAMMPrice(yrtAddress, usdcAddress);
// Price order competitively
const orderPrice = ammPrice * 0.98; // 2% better than AMM2. Sufficient Approval
// Approve exact amount or unlimited
yrtToken.approve(secondaryMarket, type(uint256).max);3. Monitor Order Status
// Check if order filled
const status = await secondaryMarket.getOrderStatus(orderHash);
if (status === 1) {
console.log("Order filled!");
}4. Cancel Stale Orders
// Cancel after 24 hours if not filled
setTimeout(async () => {
const status = await secondaryMarket.getOrderStatus(orderHash);
if (status === 0) { // Still NONE
await secondaryMarket.cancelOrder(order);
}
}, 24 * 60 * 60 * 1000);For Takers (Order Fillers)
1. Verify Order Validity
// Check maker has sufficient balance
const balance = await makerToken.balanceOf(order.maker);
if (balance < order.makerAmount) {
console.warn("Maker has insufficient balance!");
}
// Check maker approved contract
const allowance = await makerToken.allowance(order.maker, secondaryMarket);
if (allowance < order.makerAmount) {
console.warn("Maker needs to approve contract!");
}2. Compare with AMM
// Only fill if better than AMM
const ammQuote = await router.getAmountsOut(...);
const orderPrice = order.takerAmount / order.makerAmount;
const ammPrice = ammQuote[1] / ammQuote[0];
if (orderPrice < ammPrice * 1.01) { // At least 1% better
await secondaryMarket.executeSwap(order, signature);
}Gas Optimization
Order Creation (Off-Chain)
Gas Cost: 0 ETH
Signing happens in wallet (free)
No blockchain transaction required
Orders stored off-chain
Order Execution
Gas Cost: ~100,000 - 150,000 gas
Breakdown:
Signature verification: ~5,000 gas
State updates: ~25,000 gas
Token transfers (2x): ~100,000 gas
Event emission: ~2,000 gas
Optimization Tips:
Batch multiple small orders into one large order
Use unlimited approval (approve once, trade many times)
Execute during low network congestion
Liquidity Dynamics
How Both Markets Interact
Arbitrage Creates Price Parity:
Scenario: AMM price deviates from Secondary Market
AMM: 1.0 USDC/YRT
Secondary Market Order: 0.95 USDC/YRT
Arbitrageur Action:
1. Fill Secondary Market order (buy YRT at 0.95)
2. Sell YRT on AMM (sell at 1.0)
3. Profit: 0.05 USDC/YRT (minus gas)
Result:
- Secondary Market order filled
- AMM price moves toward equilibrium
- Both markets stay alignedLiquidity Flow:
Advanced Features
Partial Fills (Future Enhancement)
Current: All-or-nothing orders Future: Partially fillable orders
struct SwapOrder {
// ... existing fields ...
uint256 minFillAmount; // Minimum partial fill
uint256 filledAmount; // Track partial fills
}Order Expiration (Future Enhancement)
struct SwapOrder {
// ... existing fields ...
uint256 expiry; // Unix timestamp
}
// Validation
if (block.timestamp > order.expiry) {
revert OrderExpired();
}Fee Structure (Future Enhancement)
// Optional maker/taker fees
uint256 public makerFee = 0; // 0% initially
uint256 public takerFee = 10; // 0.1% for takersTroubleshooting
Common Errors
1. InvalidSignature
Cause: Signature doesn't match order maker
Fix: Ensure signer address matches order.maker2. OrderAlreadyFilled
Cause: Attempting to execute already-filled order
Fix: Check order status before execution3. OrderAlreadyCancelled
Cause: Maker cancelled the order
Fix: Remove from order book UI4. Insufficient Allowance
Cause: Token approval too low
Fix: Call approve() with sufficient amountDebugging Tips
// 1. Verify order hash
const orderHash = await computeOrderHash(order);
console.log("Order Hash:", orderHash);
// 2. Check order status
const status = await secondaryMarket.getOrderStatus(orderHash);
console.log("Status:", status); // 0=NONE, 1=FILLED, 2=CANCELLED
// 3. Verify signature
const recovered = ethers.verifyTypedData(domain, types, order, signature);
console.log("Signer:", recovered);
console.log("Maker:", order.maker);
console.log("Match:", recovered === order.maker);
// 4. Check balances
const makerBalance = await makerToken.balanceOf(order.maker);
const takerBalance = await takerToken.balanceOf(takerAddress);
console.log("Maker has:", makerBalance);
console.log("Taker has:", takerBalance);Deployment Information
Contract: SecondaryMarket.sol Network: Base Sepolia Testnet Address: [See technical/addresses.md] Verified: ✅ Yes
Constructor Parameters:
None (EIP-712 domain set internally)
Deployment Script:
forge create SecondaryMarket \
--rpc-url $BASE_SEPOLIA_RPC \
--private-key $PRIVATE_KEY \
--verifyRelated Documentation
Owna-DEX Overview - AMM trading mechanism
YRT Factory Overview - Token creation
User Trading Guide - Complete trading guide
Technical Reference - Contract addresses
Summary
The Secondary Market complements Owna-DEX by providing:
✅ Zero-fee P2P trading for cost-conscious users
✅ Custom pricing for negotiated deals
✅ Large order support without slippage
✅ Limit order functionality for strategic trading
✅ OTC settlement for institutional trades
Together with AMM, creates complete trading ecosystem:
DEX = Instant liquidity + always available
Secondary Market = Custom pricing + zero fees
Result: Maximum flexibility, optimal execution for all trade sizes.
Built with EIP-712 🔏 Secured by OpenZeppelin 🛡️ Zero Protocol Fees 💰
Peer-to-peer trading at your price—bringing flexibility to tokenized real estate.
Last updated
