ClashEscrow
0xc1857ad0d01bec12421fb02c577fa5e1829d648a
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
5,218 bytes
ABI entries
52
16 read · 7 write
License
none
Contract information
- Address
- 0xc1857ad0d01bec12421fb02c577fa5e1829d648a
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x6d606cc634…2C7B835Bb9
- Creation tx
- 0x308636c8fc…64d63aa5d7
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (16)
BPS() → uint16
BRONZE_WAGER() → uint256
DEPOSIT_STALE() → uint256
GOLD_WAGER() → uint256
SILVER_WAGER() → uint256
VAULT_FEE_BPS() → uint16
deposits(uint256) → address, uint8, uint256, uint256, uint64, bool
eip712Domain() → bytes1, string, string, uint256, address, bytes32, uint256[]
nextDepositId() → uint256
operator() → address
owner() → address
payoutFor(uint8) → uint256, uint256
settledMatches(bytes32) → bool
treasury() → address
uru() → address
wagerFor(uint8) → uint256
Events (7)
DepositCreatedDepositRefundedEIP712DomainChangedMatchSettledOperatorChangedOwnershipTransferredTreasuryChanged
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "uru_",
"type": "address"
},
{
"internalType": "address",
"name": "operator_",
"type": "address"
},
{
"internalType": "address",
"name": "treasury_",
"type": "address"
},
{
"internalType": "address",
"name": "initialOwner",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "DepositAlreadySpent",
"type": "error"
},
{
"inputs": [],
"name": "DepositNotStale",
"type": "error"
},
{
"inputs": [],
"name": "DepositUnknown",
"type": "error"
},
{
"inputs": [],
"name": "ECDSAInvalidSignature",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "length",
"type": "uint256"
}
],
"name": "ECDSAInvalidSignatureLength",
"type": "error"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "s",
"type": "bytes32"
}
],
"name": "ECDSAInvalidSignatureS",
"type": "error"
},
{
"inputs": [],
"name": "InvalidShortString",
"type": "error"
},
{
"inputs": [],
"name": "InvalidSignature",
"type": "error"
},
{
"inputs": [],
"name": "InvalidTier",
"type": "error"
},
{
"inputs": [],
"name": "LoserNotDepositor",
"type": "error"
},
{
"inputs": [],
"name": "MatchAlreadySettled",
"type": "error"
},
{
"inputs": [],
"name": "MismatchedWager",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "OwnableInvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "OwnableUnauthorizedAccount",
"type": "error"
},
{
"inputs": [],
"name": "PayoutMismatch",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "SignatureExpired",
"type": "error"
},
{
"inputs": [
{
"internalType": "string",
"name": "str",
"type": "string"
}
],
"name": "StringTooLong",
"type": "error"
},
{
"inputs": [],
"name": "WinnerNotDepositor",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "depositId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "player",
"type": "address"
},
{
"indexed": false,
"internalType": "enum ClashEscrow.Tier",
"name": "tier",
"type": "uint8"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wager",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "DepositCreated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "depositId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "player",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wager",
"type": "uint256"
}
],
"name": "DepositRefunded",
"type": "event"
},
{
"anonymous": false,
"inputs": [],
"name": "EIP712DomainChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "matchId",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "address",
"name": "winner",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "payout",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "vaultCut",
"type": "uint256"
}
],
"name": "MatchSettled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "OperatorChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "treasury",
"type": "address"
}
],
"name": "TreasuryChanged",
"type": "event"
},
{
"inputs": [],
"name": "BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "BRONZE_WAGER",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "DEPOSIT_STALE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "GOLD_WAGER",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "SILVER_WAGER",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "VAULT_FEE_BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "enum ClashEscrow.Tier",
"name": "tier",
"type": "uint8"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "deposit",
"outputs": [
{
"internalType": "uint256",
"name": "depositId",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "deposits",
"outputs": [
{
"internalType": "address",
"name": "player",
"type": "address"
},
{
"internalType": "enum ClashEscrow.Tier",
"name": "tier",
"type": "uint8"
},
{
"internalType": "uint256",
"name": "wager",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "uint64",
"name": "createdAt",
"type": "uint64"
},
{
"internalType": "bool",
"name": "spent",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "eip712Domain",
"outputs": [
{
"internalType": "bytes1",
"name": "fields",
"type": "bytes1"
},
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "version",
"type": "string"
},
{
"internalType": "uint256",
"name": "chainId",
"type": "uint256"
},
{
"internalType": "address",
"name": "verifyingContract",
"type": "address"
},
{
"internalType": "bytes32",
"name": "salt",
"type": "bytes32"
},
{
"internalType": "uint256[]",
"name": "extensions",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "nextDepositId",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "operator",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "enum ClashEscrow.Tier",
"name": "tier",
"type": "uint8"
}
],
"name": "payoutFor",
"outputs": [
{
"internalType": "uint256",
"name": "winnerPayout",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "vaultCut",
"type": "uint256"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "depositId",
"type": "uint256"
}
],
"name": "refund",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOperator",
"type": "address"
}
],
"name": "setOperator",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newTreasury",
"type": "address"
}
],
"name": "setTreasury",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "bytes32",
"name": "matchId",
"type": "bytes32"
},
{
"internalType": "address",
"name": "winner",
"type": "address"
},
{
"internalType": "address",
"name": "loser",
"type": "address"
},
{
"internalType": "uint256",
"name": "winnerDepositId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "loserDepositId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "winnerPayout",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "nonce",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"internalType": "struct ClashEscrow.MatchResult",
"name": "r",
"type": "tuple"
},
{
"internalType": "bytes",
"name": "sig",
"type": "bytes"
}
],
"name": "settleMatch",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"name": "settledMatches",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "treasury",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "uru",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "enum ClashEscrow.Tier",
"name": "tier",
"type": "uint8"
}
],
"name": "wagerFor",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "pure",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
/// @title ClashEscrow
/// @notice URU escrow for Fleece & Fangs PvP wagers. Both players deposit
/// at a chosen tier; when the server certifies the winner (EIP-712
/// signed MatchResult from a pinned `operator` key), the winner
/// withdraws the pot net of a vault fee.
/// @dev ── Vault fee routing (v1) ──
/// The vault fee is transferred to `treasury` — expected to be the
/// project Safe multisig at deploy. The Safe accumulates URU and
/// directs it into emissions manually (e.g. topping up a next
/// YieldReserve epoch, or a buy-and-burn cycle). This indirection
/// exists because `ChibiLiquidityVault.pushSubsidy` currently
/// restricts msg.sender to `yieldReserve`. If a future vault
/// version exposes an `authorizedPusher` role, ClashEscrow can
/// push directly by pointing `treasury` at a `SubsidyRouter`
/// helper — no changes required here.
contract ClashEscrow is Ownable, ReentrancyGuard, EIP712 {
using SafeERC20 for IERC20;
// ─── Tier config ────────────────────────────────────────────────
enum Tier { BRONZE, SILVER, GOLD }
uint256 public constant BRONZE_WAGER = 1_000 ether;
uint256 public constant SILVER_WAGER = 5_000 ether;
uint256 public constant GOLD_WAGER = 10_000 ether;
/// @dev 500 bps = 5% of the 2× pot flows to the emissions vault.
uint16 public constant VAULT_FEE_BPS = 500;
uint16 public constant BPS = 10_000;
/// @dev How long a deposit must sit before `refund()` can pull it back.
/// Set generously (4 h) because a live match's deposit is NOT
/// distinguished on-chain from an unmatched one: a shorter window
/// would let a losing player refund mid-match and strand the
/// winner's pot. v2 can add a signed `commit(depositIds, matchId)`
/// from the operator so matched deposits are frozen against refund
/// for the full match duration; until then, 4 h is the safety margin.
uint256 public constant DEPOSIT_STALE = 4 hours;
IERC20 public immutable uru;
address public operator;
address public treasury;
struct DepositRecord {
address player;
Tier tier;
uint256 wager;
uint256 tokenId;
uint64 createdAt;
bool spent; // true after settleMatch or refund
}
mapping(uint256 => DepositRecord) public deposits;
mapping(bytes32 => bool) public settledMatches;
uint256 public nextDepositId = 1;
// ─── EIP-712 ────────────────────────────────────────────────────
bytes32 private constant MATCH_RESULT_TYPEHASH = keccak256(
"MatchResult(bytes32 matchId,address winner,address loser,uint256 winnerDepositId,uint256 loserDepositId,uint256 winnerPayout,uint256 nonce,uint256 deadline)"
);
struct MatchResult {
bytes32 matchId;
address winner;
address loser;
uint256 winnerDepositId;
uint256 loserDepositId;
uint256 winnerPayout;
uint256 nonce;
uint256 deadline;
}
// ─── Events ─────────────────────────────────────────────────────
event DepositCreated(uint256 indexed depositId, address indexed player, Tier tier, uint256 wager, uint256 tokenId);
event MatchSettled(bytes32 indexed matchId, address indexed winner, uint256 payout, uint256 vaultCut);
event DepositRefunded(uint256 indexed depositId, address indexed player, uint256 wager);
event OperatorChanged(address indexed operator);
event TreasuryChanged(address indexed treasury);
// ─── Errors ─────────────────────────────────────────────────────
error ZeroAddress();
error InvalidTier();
error DepositAlreadySpent();
error DepositUnknown();
error DepositNotStale();
error MatchAlreadySettled();
error InvalidSignature();
error SignatureExpired();
error MismatchedWager();
error WinnerNotDepositor();
error LoserNotDepositor();
error PayoutMismatch();
constructor(address uru_, address operator_, address treasury_, address initialOwner)
Ownable(initialOwner)
EIP712("ClashEscrow", "1")
{
if (uru_ == address(0) || operator_ == address(0) || treasury_ == address(0) || initialOwner == address(0)) {
revert ZeroAddress();
}
uru = IERC20(uru_);
operator = operator_;
treasury = treasury_;
}
// ─── Admin ──────────────────────────────────────────────────────
function setOperator(address newOperator) external onlyOwner {
if (newOperator == address(0)) revert ZeroAddress();
operator = newOperator;
emit OperatorChanged(newOperator);
}
function setTreasury(address newTreasury) external onlyOwner {
if (newTreasury == address(0)) revert ZeroAddress();
treasury = newTreasury;
emit TreasuryChanged(newTreasury);
}
// ─── Views ──────────────────────────────────────────────────────
function wagerFor(Tier tier) public pure returns (uint256) {
if (tier == Tier.BRONZE) return BRONZE_WAGER;
if (tier == Tier.SILVER) return SILVER_WAGER;
if (tier == Tier.GOLD) return GOLD_WAGER;
revert InvalidTier();
}
function payoutFor(Tier tier) public pure returns (uint256 winnerPayout, uint256 vaultCut) {
uint256 pot = wagerFor(tier) * 2;
vaultCut = (pot * VAULT_FEE_BPS) / BPS;
winnerPayout = pot - vaultCut;
}
// ─── Player actions ─────────────────────────────────────────────
/// @notice Deposit URU at the given tier and receive a depositId to hand
/// to the matchmaker. Caller must approve URU beforehand.
function deposit(Tier tier, uint256 tokenId) external nonReentrant returns (uint256 depositId) {
uint256 wager = wagerFor(tier);
depositId = nextDepositId++;
deposits[depositId] = DepositRecord({
player: msg.sender,
tier: tier,
wager: wager,
tokenId: tokenId,
createdAt: uint64(block.timestamp),
spent: false
});
uru.safeTransferFrom(msg.sender, address(this), wager);
emit DepositCreated(depositId, msg.sender, tier, wager, tokenId);
}
/// @notice Settle a completed match. The operator-signed MatchResult
/// authorizes the pot payout to the winner. `settledMatches`
/// ensures a signature can be replayed at most once.
function settleMatch(MatchResult calldata r, bytes calldata sig) external nonReentrant {
if (settledMatches[r.matchId]) revert MatchAlreadySettled();
if (block.timestamp > r.deadline) revert SignatureExpired();
bytes32 structHash = keccak256(
abi.encode(
MATCH_RESULT_TYPEHASH,
r.matchId,
r.winner,
r.loser,
r.winnerDepositId,
r.loserDepositId,
r.winnerPayout,
r.nonce,
r.deadline
)
);
bytes32 digest = _hashTypedDataV4(structHash);
if (ECDSA.recover(digest, sig) != operator) revert InvalidSignature();
DepositRecord storage w = deposits[r.winnerDepositId];
DepositRecord storage l = deposits[r.loserDepositId];
if (w.player == address(0) || l.player == address(0)) revert DepositUnknown();
if (w.spent || l.spent) revert DepositAlreadySpent();
if (w.player != r.winner) revert WinnerNotDepositor();
if (l.player != r.loser) revert LoserNotDepositor();
if (w.wager != l.wager) revert MismatchedWager();
uint256 pot = w.wager * 2;
uint256 fee = (pot * VAULT_FEE_BPS) / BPS;
uint256 expected = pot - fee;
if (r.winnerPayout != expected) revert PayoutMismatch();
w.spent = true;
l.spent = true;
settledMatches[r.matchId] = true;
// Vault emissions fee → treasury (Safe). Post-launch, retarget to a
// SubsidyRouter contract that can push into the live vault.
if (fee > 0) uru.safeTransfer(treasury, fee);
uru.safeTransfer(r.winner, expected);
emit MatchSettled(r.matchId, r.winner, expected, fee);
}
/// @notice Refund an unmatched deposit after DEPOSIT_STALE. Permissionless
/// — URU always returns to the depositor.
function refund(uint256 depositId) external nonReentrant {
DepositRecord storage d = deposits[depositId];
if (d.player == address(0)) revert DepositUnknown();
if (d.spent) revert DepositAlreadySpent();
if (block.timestamp < d.createdAt + DEPOSIT_STALE) revert DepositNotStale();
d.spent = true;
uru.safeTransfer(d.player, d.wager);
emit DepositRefunded(depositId, d.player, d.wager);
}
}
Chain explorer4874msChain node85ms