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

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