Building trust-minimized prediction markets with logarithmic market scoring rules in Arkade
contract LMSRPredictionMarket(
// Market parameters (immutable)
bytes32[] outcomeHashes,
int liquidityParameter,
int resolutionTime,
pubkey oracle,
// State parameters (change with each transaction)
int[] outcomeShares, // Share count for each outcome
int totalExpSum, // Current exponential sum
bool isResolved, // Resolution status
int winningOutcome // Winner (if resolved)
) {
// Buy outcome shares
function buyShares(
int outcome,
int quantity,
int maxCost,
signature userSig,
pubkey user
) {
// Verify outcome index is valid
require(outcome >= 0 && outcome < outcomeHashes.length, "Invalid outcome");
require(quantity > 0, "Quantity must be positive");
// Inspect current UTXO state
require(tx.inputs.length == 2, "Must have exactly 2 inputs");
require(tx.outputs.length >= 2, "Must have at least 2 outputs");
// Get current share count for this outcome from constructor parameters
int currentShare = outcomeShares[outcome];
// Calculate new share amount and exponential sum
int newShare = currentShare + quantity;
int oldExp = expApprox(currentShare, liquidityParameter);
int newExp = expApprox(newShare, liquidityParameter);
int newExpSum = totalExpSum - oldExp + newExp;
// Calculate trade cost using LMSR
int cost = liquidityParameter * (lnApprox(newExpSum) - lnApprox(totalExpSum));
require(cost <= maxCost, "Cost exceeds maximum");
require(cost > 0, "Invalid trade cost");
// Verify payment input
require(tx.inputs[1].value >= cost, "Insufficient payment");
// Create new contract instance with updated state
int[] newOutcomeShares = outcomeShares;
newOutcomeShares[outcome] = newShare;
bytes newCovenantScript = new LMSRPredictionMarket(
outcomeHashes, // Same market parameters
liquidityParameter,
resolutionTime,
oracle,
newOutcomeShares, // Updated shares
newExpSum, // Updated exponential sum
false, // Still not resolved
-1 // No winner yet
);
// Calculate taproot commitment for new covenant
bytes32 scriptHash = sha256(concat(<0xC4>, newCovenantScript));
bytes32 tapTweak = sha256(concat("TapTweak/arkade", UNSPENDABLE_KEY, scriptHash));
// Verify output 0 continues the covenant with new state
require(tx.outputs[0].scriptPubKey.length == 34, "Invalid taproot output");
require(tx.outputs[0].scriptPubKey[0] == 0x51, "Must be witness v1");
require(tx.outputs[0].scriptPubKey[1] == 0x20, "Must be 32-byte program");
// Clean, composable covenant verification
bytes32 outputPubkey = tx.outputs[0].scriptPubKey[2:34];
require(tweakVerify(outputPubkey, UNSPENDABLE_KEY, tapTweak), "Invalid covenant continuation");
// Verify pool value increases by cost
require(tx.outputs[0].value == tx.inputs[0].value + cost, "Pool value mismatch");
// Verify user receives outcome tokens
bytes32 outcomeAsset = deriveOutcomeAsset(outcome);
require(tx.outputs[1].asset == outcomeAsset, "Wrong outcome asset");
require(tx.outputs[1].value == quantity, "Token quantity mismatch");
// Create user output script
require(tx.outputs[1].scriptPubKey.length == 34, "Invalid user output");
require(tx.outputs[1].scriptPubKey[0] == 0x51, "Must be witness v1");
require(tx.outputs[1].scriptPubKey[1] == 0x20, "Must be 32-byte program");
// Verify user signature
require(checkSig(userSig, user), "Invalid user signature");
}
// Sell outcome shares
function sellShares(
int outcome,
int quantity,
int minPayout,
signature userSig,
pubkey user
) {
// Verify outcome index and quantity
require(outcome >= 0 && outcome < outcomeHashes.length, "Invalid outcome");
require(quantity > 0, "Quantity must be positive");
// Inspect current state
require(tx.inputs.length == 2, "Must have exactly 2 inputs");
require(tx.outputs.length >= 2, "Must have at least 2 outputs");
// Get current market state from constructor parameters
int currentShare = outcomeShares[outcome];
// Verify sufficient shares exist
require(currentShare >= quantity, "Insufficient shares in market");
// Calculate new state
int newShare = currentShare - quantity;
int oldExp = expApprox(currentShare, liquidityParameter);
int newExp = expApprox(newShare, liquidityParameter);
int newExpSum = totalExpSum - oldExp + newExp;
// Calculate payout (negative cost for selling)
int payout = liquidityParameter * (lnApprox(totalExpSum) - lnApprox(newExpSum));
require(payout >= minPayout, "Payout below minimum");
require(payout > 0, "Invalid payout amount");
// Verify user is burning correct outcome tokens
bytes32 outcomeAsset = deriveOutcomeAsset(outcome);
require(tx.inputs[1].asset == outcomeAsset, "Wrong outcome asset");
require(tx.inputs[1].value >= quantity, "Insufficient tokens to burn");
// Create new contract instance with updated state
int[] newOutcomeShares = outcomeShares;
newOutcomeShares[outcome] = newShare;
bytes newCovenantScript = new LMSRPredictionMarket(
outcomeHashes,
liquidityParameter,
resolutionTime,
oracle,
newOutcomeShares, // Updated shares
newExpSum, // Updated exponential sum
false, // Still not resolved
-1 // No winner yet
);
bytes32 scriptHash = sha256(concat(<0xC4>, newCovenantScript));
bytes32 tapTweak = sha256(concat("TapTweak/arkade", UNSPENDABLE_KEY, scriptHash));
// Verify covenant continuation
require(tx.outputs[0].scriptPubKey.length == 34, "Invalid taproot output");
require(tx.outputs[0].scriptPubKey[0] == 0x51, "Must be witness v1");
require(tx.outputs[0].scriptPubKey[1] == 0x20, "Must be 32-byte program");
bytes32 outputPubkey = tx.outputs[0].scriptPubKey[2:34];
require(tweakVerify(outputPubkey, UNSPENDABLE_KEY, tapTweak), "Invalid covenant continuation");
// Verify pool value decreases by payout
require(tx.outputs[0].value == tx.inputs[0].value - payout, "Pool value mismatch");
// Verify user receives payout
require(tx.outputs[1].value == payout, "Payout amount mismatch");
require(tx.outputs[1].scriptPubKey.length == 34, "Invalid payout output");
require(tx.outputs[1].scriptPubKey[0] == 0x51, "Must be witness v1");
// Verify user signature
require(checkSig(userSig, user), "Invalid user signature");
}
// Resolve market with oracle signature
function resolve(
int resolvedWinningOutcome,
bytes resolutionData,
signature oracleSig
) {
// Verify resolution time has passed
require(tx.time >= resolutionTime, "Resolution time not reached");
// Verify winning outcome is valid
require(resolvedWinningOutcome >= 0 && resolvedWinningOutcome < outcomeHashes.length, "Invalid winning outcome");
// Verify oracle signature on resolution data
bytes message = sha256(concat(outcomeHashes[resolvedWinningOutcome], resolutionData, int2bytes(tx.time)));
require(checkSigFromStack(oracleSig, oracle, message), "Invalid oracle signature");
// Get current market state
int totalWinningShares = outcomeShares[resolvedWinningOutcome];
require(totalWinningShares > 0, "No winning shares exist");
// Create resolved covenant state
bytes resolvedCovenantScript = new LMSRPredictionMarket(
outcomeHashes,
liquidityParameter,
resolutionTime,
oracle,
outcomeShares, // Preserve final shares
totalExpSum, // Preserve final exp sum
true, // Mark as resolved
resolvedWinningOutcome // Set winner
);
bytes32 resolvedScriptHash = sha256(concat(<0xC4>, resolvedCovenantScript));
bytes32 resolvedTapTweak = sha256(concat("TapTweak/arkade", UNSPENDABLE_KEY, resolvedScriptHash));
// Verify resolved covenant output
require(tx.outputs[0].scriptPubKey.length == 34, "Invalid taproot output");
require(tx.outputs[0].scriptPubKey[0] == 0x51, "Must be witness v1");
require(tx.outputs[0].scriptPubKey[1] == 0x20, "Must be 32-byte program");
bytes32 resolvedPubkey = tx.outputs[0].scriptPubKey[2:34];
require(tweakVerify(resolvedPubkey, UNSPENDABLE_KEY, resolvedTapTweak), "Invalid resolution state");
// Preserve pool value for redemptions
require(tx.outputs[0].value == tx.inputs[0].value, "Pool value must be preserved");
}
// Redeem winning shares after resolution
function redeem(
int redeemShares,
signature userSig,
pubkey user
) {
// Verify this is a resolved market
require(isResolved, "Market not resolved");
int totalWinningShares = outcomeShares[winningOutcome];
int poolValue = tx.inputs[0].value;
// Calculate proportional payout using clean arithmetic
int payout = (redeemShares * poolValue) / totalWinningShares;
// Verify user is burning winning outcome tokens
bytes32 winningAsset = deriveOutcomeAsset(winningOutcome);
require(tx.inputs[1].asset == winningAsset, "Wrong outcome asset");
require(tx.inputs[1].value >= redeemShares, "Insufficient winning shares");
// Verify payout to user
require(tx.outputs[0].value == payout, "Payout amount mismatch");
require(tx.outputs[0].scriptPubKey.length == 34, "Invalid payout output");
require(tx.outputs[0].scriptPubKey[0] == 0x51, "Must be witness v1");
// Update remaining pool if there are still shares to redeem
int remainingShares = totalWinningShares - redeemShares;
if (remainingShares > 0) {
int remainingPool = poolValue - payout;
// Create updated resolved covenant with reduced shares
int[] updatedShares = outcomeShares;
updatedShares[winningOutcome] = remainingShares;
bytes updatedResolvedScript = new LMSRPredictionMarket(
outcomeHashes,
liquidityParameter,
resolutionTime,
oracle,
updatedShares, // Updated shares with redemption
totalExpSum, // Preserve exp sum
true, // Still resolved
winningOutcome // Same winner
);
bytes32 updatedScriptHash = sha256(concat(<0xC4>, updatedResolvedScript));
bytes32 updatedTapTweak = sha256(concat("TapTweak/arkade", UNSPENDABLE_KEY, updatedScriptHash));
require(tx.outputs[1].scriptPubKey.length == 34, "Invalid continuation output");
require(tx.outputs[1].scriptPubKey[0] == 0x51, "Must be witness v1");
require(tx.outputs[1].scriptPubKey[1] == 0x20, "Must be 32-byte program");
bytes32 updatedPubkey = tx.outputs[1].scriptPubKey[2:34];
require(tweakVerify(updatedPubkey, UNSPENDABLE_KEY, updatedTapTweak), "Invalid continuation");
require(tx.outputs[1].value == remainingPool, "Remaining pool value mismatch");
}
// Verify user signature
require(checkSig(userSig, user), "Invalid user signature");
}
// Mathematical helper functions using clean arithmetic operations
function expApprox(int x, int b) internal returns (int) {
if (x == 0) return 10000; // Fixed point 1.0
// Taylor series: e^(x/b) ≈ 1 + x/b + (x/b)²/2 + (x/b)³/6
// Compiler automatically uses OP_MUL64, OP_DIV64 etc for overflow safety
int term1 = (x * 10000) / b;
int xOverB = x / b;
int term2 = (xOverB * xOverB) / 2;
int term3 = (xOverB * xOverB * xOverB) / 6;
return 10000 + term1 + term2 + term3;
}
function lnApprox(int x) internal returns (int) {
require(x > 0, "Cannot take log of non-positive number");
if (x == 10000) return 0; // ln(1) = 0
// Series expansion: ln(x) ≈ (x-1) - (x-1)²/2 + (x-1)³/3
// Compiler handles 64-bit arithmetic automatically
int diff = x - 10000;
int term1 = diff;
int term2 = (diff * diff) / 20000;
int term3 = (diff * diff * diff) / 300000000;
return term1 + term3 - term2;
}
function deriveOutcomeAsset(int outcome) internal returns (bytes32) {
// Derive unique asset ID for each outcome
return sha256(concat("OUTCOME_TOKEN_", int2bytes(outcome)));
}
}
function adjustLiquidity(int volumeMetric, int timeDecay) internal returns (int) {
// Increase liquidity during high volume periods
int baseAdjustment = liquidityParameter;
if (volumeMetric > highVolumeThreshold) {
baseAdjustment = (baseAdjustment * 13000) / 10000; // 30% increase
}
// Apply time decay to encourage resolution
int decayFactor = (timeDecay * 9900) / 10000; // 1% decay per period
int adjustedLiquidity = (baseAdjustment * decayFactor) / 10000;
// Ensure bounds
require(adjustedLiquidity >= minLiquidity, "Liquidity too low");
require(adjustedLiquidity <= maxLiquidity, "Liquidity too high");
return adjustedLiquidity;
}