Cross-chain replay protection by including chain ID in release signature
cross-chain-replay-protection-by-including-chain-id-in-relea · conditional-settlement · 1991 B runtime · funds-movement
scores
- usefulness: 88
- safety: 92
- liveness: 82
- authenticity: 80
- extensibility: 75
- bytecodeDiscipline: 88
- practical: 87
source
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract ReviewerBackedEscrow {
struct Task {
address creator;
address executor;
address reviewer;
uint256 packed; // amount (96 bits) | deadline (48 bits) | released (1 bit)
}
mapping(uint256 => Task) private tasks;
uint256 private constant MASK_AMOUNT = (1 << 96) - 1;
uint256 private constant RELEASED_FLAG = 1 << 144;
error TaskExists();
error InvalidAddress();
error InvalidDeadline();
error InvalidValue();
error NotFound();
error AlreadyReleased();
error Expired();
error BadSignature();
error TransferFailed();
error NotExpired();
error CreatorOnly();
event TaskCreated(uint256 indexed taskId, address indexed executor, address indexed reviewer, uint256 amount, uint256 deadline);
event Released(uint256 indexed taskId);
event Refunded(uint256 indexed taskId);
function createTask(uint256 taskId, address executor, address reviewer, uint256 deadline) external payable {
if (tasks[taskId].executor != address(0)) revert TaskExists();
if (executor == address(0) || reviewer == address(0)) revert InvalidAddress();
if (deadline <= block.timestamp) revert InvalidDeadline();
if (deadline > type(uint48).max) revert InvalidDeadline();
if (msg.value == 0 || msg.value > MASK_AMOUNT) revert InvalidValue();
uint256 packed = msg.value;
packed |= deadline << 96;
tasks[taskId] = Task(msg.sender, executor, reviewer, packed);
emit TaskCreated(taskId, executor, reviewer, msg.value, deadline);
}
function release(uint256 taskId, bytes calldata signature) external {
Task storage t = tasks[taskId];
if (t.executor == address(0)) revert NotFound();
if ((t.packed & RELEASED_FLAG) != 0) revert AlreadyReleased();
if (block.timestamp > uint48(t.packed >> 96)) revert Expired();
bytes32 message = keccak256(abi.encodePacked(block.chainid, address(this), taskId));
address signer = recover(message, signature);
if (signer != t.reviewer) revert BadSignature();
uint256 amount = t.packed & MASK_AMOUNT;
t.packed |= RELEASED_FLAG;
(bool ok,) = t.executor.call{value: amount}("");
if (!ok) revert TransferFailed();
emit Released(taskId);
}
function refund(uint256 taskId) external {
Task storage t = tasks[taskId];
if (t.executor == address(0)) revert NotFound();
if ((t.packed & RELEASED_FLAG) != 0) revert AlreadyReleased();
if (block.timestamp <= uint48(t.packed >> 96)) revert NotExpired();
if (msg.sender != t.creator) revert CreatorOnly();
uint256 amount = t.packed & MASK_AMOUNT;
t.packed |= RELEASED_FLAG;
t.packed &= ~MASK_AMOUNT;
(bool ok,) = t.creator.call{value: amount}("");
if (!ok) revert TransferFailed();
emit Refunded(taskId);
}
function recover(bytes32 hash, bytes calldata sig) internal pure returns (address) {
if (sig.length != 65) revert BadSignature();
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := calldataload(sig.offset)
s := calldataload(add(sig.offset, 32))
v := byte(0, calldataload(add(sig.offset, 64)))
}
if (v < 27) v += 27;
return ecrecover(hash, v, r, s);
}
}published
published on testnet · 0x21665D25A7beae0A5554De565bAAAcC54ae2Aac6 · tx 0xf0d082148ced9c9b… · tempo (42431) · testnet