SMART CONTRACTS
TabEscrowRouter
Holds funds in escrow until the recipient claims, or the sender refunds after a deadline. Two claim paths: Handle (recipient must own a Tab handle, executor releases) and Code (anyone with the secret can claim — magic share links / claim-by-link). Backs the OpenTab product.
Surface
// --- create ---
function createEscrow(
string calldata handleSlug,
uint256 amount,
uint256 deadline,
uint256 senderNonce
) external returns (bytes32 id);
function createEscrowByCode(
bytes32 codeHash, // keccak256(bytes(secret))
uint256 amount,
uint256 deadline,
uint256 senderNonce
) external returns (bytes32 id);
// --- claim ---
function claim(
bytes32 id,
string calldata handleSlug,
address recipient
) external onlyExecutor; // handle path
function claimByCode(
bytes32 id,
bytes calldata secret
) external; // permissionless: caller receives
function claimByCodeFor(
bytes32 id,
address recipient,
bytes calldata secret
) external onlyExecutor; // sponsored: caller pays gas, recipient receives
// --- refund (always permissionless after deadline) ---
function refund(bytes32 id) external;
// --- views ---
function escrows(bytes32 id) external view returns (
address sender,
bytes32 commitment, // handleHash OR codeHash
uint256 amount,
uint256 deadline,
uint16 feeBps, // snapshotted at create time
uint8 claimType, // 0=Handle 1=Code
uint8 status // 0=None 1=Pending 2=Claimed 3=Refunded
);
function escrowFee(bytes32 id) external view returns (uint256 fee, uint256 net);
function computeHandleId(address sender, string calldata handleSlug, uint256 senderNonce)
external pure returns (bytes32);
function computeCodeId(address sender, bytes32 codeHash, uint256 senderNonce)
external pure returns (bytes32);ID derivation
id = keccak256(abi.encode(sender, commitment, senderNonce, claimType)). The claimType byte is part of the id, so a handle and code escrow with the same commitment + nonce will never collide. Off-chain code can compute the id without a chain call.
Trust model
- Custody is on-chain. Funds sit in the router until
claim*orrefundmoves them. - Handle path.Tab's executor allowlist is the only caller permitted to release a handle escrow — and only to the address that owns the committed handle. Compromise of one executor key doesn't let it pay itself.
- Code path. Anyone presenting the preimage of the committed
codeHashcan callclaimByCodeand receive the funds in their own wallet — single-use, the escrow flips toClaimedon the first valid reveal. Possessing the secret IS the authorization, so the sender must treat the share link like cash. - Sponsored code claims.
claimByCodeForlets an executor pay gas on behalf of a recipient — used for "click link → instant funds" flows where the recipient has no native gas yet. - Fee. Snapshotted on the escrow at create time (default 1%). Later admin fee changes never retroactively touch an open escrow.
- Pause. Owner can halt NEW escrow creation in an emergency. Existing claims + refunds always work — user funds can never be trapped.
Events
event EscrowCreated(bytes32 indexed id, address indexed sender,
bytes32 indexed commitment, uint8 claimType,
string handleSlug, // empty for code escrows
uint256 amount, uint256 deadline, uint16 feeBps);
event EscrowClaimed(bytes32 indexed id, address indexed recipient,
uint256 net, uint256 fee);
event EscrowRefunded(bytes32 indexed id, address indexed sender,
uint256 amount);Source + tests
Tab/src/TabEscrowRouter.sol with a Foundry test suite covering the executor allowlist, the deadline arithmetic, claim-by-code reveal logic, fee snapshotting, and the full create / claim / refund state machine. See the contracts overview for the deployment table and audit scope.