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

Feature
Benefit
Trade-off

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

Feature
Benefit
Trade-off

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: Optimized

2. 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 → Seller

4. 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:

Status
Description
Can Execute?

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 order

Technical Implementation

Contract: SecondaryMarket.sol

Deployed at: [TBD - See technical/addresses.md]

Core Functions

1. executeSwap()

function executeSwap(
    SwapOrder calldata _order,
    bytes calldata _signature
) external nonReentrant

Executes 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:

  1. Hash order with EIP-712

  2. Recover signer from signature

  3. Verify signer == order.maker

  4. Mark order as FILLED

  5. Transfer makerToken: maker → taker

  6. Transfer takerToken: taker → maker

  7. Emit SwapExecuted event

2. cancelOrder()

function cancelOrder(SwapOrder calldata _order) external

Cancels 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:

  1. Centralized Order Book

    • Fast order matching

    • Low latency

    • Requires trusted operator

    • Example: Custom API backend

  2. Decentralized Order Relay

    • P2P order broadcasting

    • No central authority

    • Higher latency

    • Example: IPFS + libp2p

  3. 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 buyers

Buying 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 AMM

2. 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:

  1. Batch multiple small orders into one large order

  2. Use unlimited approval (approve once, trade many times)

  3. 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 aligned

Liquidity 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 takers

Troubleshooting

Common Errors

1. InvalidSignature

Cause: Signature doesn't match order maker
Fix: Ensure signer address matches order.maker

2. OrderAlreadyFilled

Cause: Attempting to execute already-filled order
Fix: Check order status before execution

3. OrderAlreadyCancelled

Cause: Maker cancelled the order
Fix: Remove from order book UI

4. Insufficient Allowance

Cause: Token approval too low
Fix: Call approve() with sufficient amount

Debugging 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 \
  --verify


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