Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion artifacts/erc20_deployment.hex

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion artifacts/erc20_runtime.hex

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion artifacts/native_deployment.hex

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion artifacts/native_runtime.hex

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion out/EscrowERC20.sol/EscrowERC20.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion out/EscrowNative.sol/EscrowNative.json

Large diffs are not rendered by default.

42 changes: 14 additions & 28 deletions src/EscrowBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,21 @@ abstract contract EscrowBase {
error CancellationRequested();
error ExecutorAlreadyBonded();
error InsufficientBond();
error CommitmentMismatch();

// The following variables are set up in the constructor.
address immutable deployerAddress;
uint256 public currentRewardAmount;
uint256 public currentPaymentAmount;
uint256 public originalRewardAmount;
uint256 public deposit; // Total deposited (original + seized bonds)
bytes32 public commitment; // H(recipient, [token,] amount, salt)

// The following variables are for Merkle proof validation
address public immutable expectedRecipient; // The intended recipient of the transfer
uint256 public immutable expectedAmount; // The expected transfer amount
uint256 public constant MAX_BLOCK_LOOKBACK = 256; // Maximum blocks to look back for validation
uint256 public constant MAX_BLOCK_LOOKBACK = 256;

// The following variables are dynamically adjusted by the contract when a bond or cancellation request is submitted.
address public bondedExecutor;
uint256 public executionDeadline;
uint256 public bondAmount;
uint256 public totalBondsDeposited;
bool public cancellationRequest;
bool public funded; // marks if the contract has funds to pay out the executors or not (if it doesn't have funds, no executor should be accepted)
bool public funded;

constructor(address _expectedRecipient, uint256 _expectedAmount) {
expectedRecipient = _expectedRecipient;
expectedAmount = _expectedAmount;
constructor() {
deployerAddress = msg.sender;
}

Expand Down Expand Up @@ -86,11 +78,10 @@ abstract contract EscrowBase {
executionDeadline = 0;
}

// Internal helper to handle expired bonds (adds bond to reward pool)
// Internal helper to handle expired bonds (adds bond to deposit pool)
function _handleExpiredBond() internal {
if (executionDeadline > 0 && block.timestamp > executionDeadline) {
currentRewardAmount += bondAmount;
totalBondsDeposited += bondAmount;
deposit += bondAmount;
_tryResetBondData();
}
}
Expand All @@ -100,7 +91,7 @@ abstract contract EscrowBase {
if (!funded) revert NotFunded();
if (cancellationRequest) revert CancellationRequested();
if (is_bonded()) revert ExecutorAlreadyBonded();
if (_bondAmount < currentRewardAmount / 2) revert InsufficientBond();
if (_bondAmount < deposit / 400) revert InsufficientBond();
}

// Internal helper to set bond data
Expand All @@ -116,13 +107,13 @@ abstract contract EscrowBase {
bondAmount = 0;
executionDeadline = 0;
funded = false;
currentPaymentAmount = 0;
currentRewardAmount = 0;
deposit = 0;
commitment = bytes32(0);
}

// Internal helper to calculate payout amount
function _calculatePayout() internal view returns (uint256) {
return bondAmount + currentRewardAmount + currentPaymentAmount;
return bondAmount + deposit;
}

// Internal helper to validate withdraw requirements
Expand All @@ -131,15 +122,10 @@ abstract contract EscrowBase {
if (msg.sender != deployerAddress) revert OnlyDeployer();
}

// Internal helper to calculate withdrawable amount and clear state
function _calculateWithdrawableAmount() internal view returns (uint256) {
return currentPaymentAmount + originalRewardAmount;
}

// Internal helper to clear state after withdraw
function _clearWithdrawState() internal {
funded = false;
currentPaymentAmount = 0;
currentRewardAmount = 0;
deposit = 0;
commitment = bytes32(0);
}
}
62 changes: 28 additions & 34 deletions src/EscrowERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@ contract EscrowERC20 is EscrowBase {
// Custom errors
error ZeroAddress();
error AlreadyFunded();
error ZeroRewardAmount();
error ZeroPaymentAmount();
error ZeroAmount();
error TokenTransferFailed();
error InvalidReceiptProof();
error InvalidTransferEvent();
error NoWithdrawableFunds();

address public immutable tokenContract; // The tokens used in the escrow
Expand All @@ -32,33 +30,27 @@ contract EscrowERC20 is EscrowBase {
uint256 logIndex; // Index of target log in receipt
}

constructor(
address _tokenContract,
address _expectedRecipient,
uint256 _expectedAmount,
uint256 _currentRewardAmount,
uint256 _currentPaymentAmount
) EscrowBase(_expectedRecipient, _expectedAmount) {
constructor(address _tokenContract, uint256 _amount, bytes32 _commitment) EscrowBase() {
if (_tokenContract == address(0)) revert ZeroAddress();
tokenContract = _tokenContract;

if (_currentRewardAmount > 0 && _currentPaymentAmount > 0) {
fund(_currentRewardAmount, _currentPaymentAmount);
if (_amount > 0) {
_fund(_amount, _commitment);
}
}

// takes currentRewardAmount + currentPaymentAmount from the deployer's balance from the tokenContract.
function fund(uint256 _currentRewardAmount, uint256 _currentPaymentAmount) public {
function fund(uint256 _amount, bytes32 _commitment) external {
if (msg.sender != deployerAddress) revert OnlyDeployer();
if (funded) revert AlreadyFunded();
if (_currentRewardAmount == 0) revert ZeroRewardAmount();
if (_currentPaymentAmount == 0) revert ZeroPaymentAmount();

currentRewardAmount = _currentRewardAmount;
originalRewardAmount = _currentRewardAmount;
currentPaymentAmount = _currentPaymentAmount;
if (!IERC20(tokenContract).transferFrom(msg.sender, address(this), originalRewardAmount + currentPaymentAmount))
{
_fund(_amount, _commitment);
}

function _fund(uint256 _amount, bytes32 _commitment) internal {
if (_amount == 0) revert ZeroAmount();

deposit = _amount;
commitment = _commitment;
if (!IERC20(tokenContract).transferFrom(msg.sender, address(this), _amount)) {
revert TokenTransferFailed();
}
funded = true;
Expand All @@ -78,8 +70,8 @@ contract EscrowERC20 is EscrowBase {
_setBondData(_bondAmount);
}

// Validates a given merkle proof against a recent block hash and checks the Transfer event's contents
function collect(ReceiptProof calldata proof, uint256 targetBlockNumber) external {
// Validates a given merkle proof against a recent block hash and verifies the commitment
function collect(ReceiptProof calldata proof, uint256 targetBlockNumber, bytes32 salt) external {
_validateBlockHeader(proof.blockHeader, targetBlockNumber);

// Extract receipts root and verify receipt inclusion
Expand All @@ -88,11 +80,13 @@ contract EscrowERC20 is EscrowBase {
revert InvalidReceiptProof();
}

// Validate the Transfer event
if (!ReceiptValidator.validateTransferInReceipt(
proof.receiptRlp, proof.logIndex, tokenContract, expectedRecipient, expectedAmount
)) {
revert InvalidTransferEvent();
// Extract transfer fields from the proven receipt
(address token, address recipient, uint256 amount) =
ReceiptValidator.extractTransferFromReceipt(proof.receiptRlp, proof.logIndex);

// Verify commitment: H(recipient, token, amount, salt)
if (keccak256(abi.encodePacked(recipient, token, amount, salt)) != commitment) {
revert CommitmentMismatch();
}

_payout();
Expand All @@ -114,19 +108,19 @@ contract EscrowERC20 is EscrowBase {
if (!success) revert TokenTransferFailed();
}

/// @notice Cancel and withdraw funds in a single transaction.
/// Reverts if a node has already bonded.
/// @notice Cancel and withdraw all funds in a single transaction.
/// Reverts if a bond is still active.
function cancelAndWithdraw() external {
cancellationRequest = true;
_validateWithdraw();
_handleExpiredBond();
_tryResetBondData();

uint256 withdrawableAmount = _calculateWithdrawableAmount();
uint256 withdrawableAmount = deposit;
if (withdrawableAmount == 0) revert NoWithdrawableFunds();

_clearWithdrawState();

if (withdrawableAmount == 0) revert NoWithdrawableFunds();

if (!IERC20(tokenContract).transfer(msg.sender, withdrawableAmount)) {
revert TokenTransferFailed();
}
Expand Down
52 changes: 21 additions & 31 deletions src/EscrowNative.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@ import "./EscrowBase.sol";

contract EscrowNative is EscrowBase {
// Custom errors
error IncorrectETHAmount();
error AlreadyFunded();
error ZeroRewardAmount();
error ZeroPaymentAmount();
error ZeroAmount();
error InvalidTxProof();
error InvalidReceiptProof();
error TxFailed();
error InvalidNativeTransfer();
error ETHTransferFailed();
error NoWithdrawableFunds();

Expand All @@ -26,31 +23,21 @@ contract EscrowNative is EscrowBase {
bytes path; // RLP-encoded index (same for both tx and receipt)
}

constructor(
address _expectedRecipient,
uint256 _expectedAmount,
uint256 _currentRewardAmount,
uint256 _currentPaymentAmount
) payable EscrowBase(_expectedRecipient, _expectedAmount) {
if (_currentRewardAmount > 0 && _currentPaymentAmount > 0) {
if (msg.value != _currentRewardAmount + _currentPaymentAmount) revert IncorrectETHAmount();
currentRewardAmount = _currentRewardAmount;
originalRewardAmount = _currentRewardAmount;
currentPaymentAmount = _currentPaymentAmount;
constructor(bytes32 _commitment) payable EscrowBase() {
if (msg.value > 0) {
deposit = msg.value;
commitment = _commitment;
funded = true;
}
}

function fund(uint256 _currentRewardAmount, uint256 _currentPaymentAmount) external payable {
function fund(bytes32 _commitment) external payable {
if (msg.sender != deployerAddress) revert OnlyDeployer();
if (funded) revert AlreadyFunded();
if (_currentRewardAmount == 0) revert ZeroRewardAmount();
if (_currentPaymentAmount == 0) revert ZeroPaymentAmount();
if (msg.value != _currentRewardAmount + _currentPaymentAmount) revert IncorrectETHAmount();
if (msg.value == 0) revert ZeroAmount();

currentRewardAmount = _currentRewardAmount;
originalRewardAmount = _currentRewardAmount;
currentPaymentAmount = _currentPaymentAmount;
deposit = msg.value;
commitment = _commitment;
funded = true;
}

Expand All @@ -65,7 +52,7 @@ contract EscrowNative is EscrowBase {

// Validates native ETH transfer by proving both transaction inclusion (for to/value)
// and receipt inclusion (for status == 1, i.e., successful execution)
function collect(NativeTransferProof calldata proof, uint256 targetBlockNumber) external {
function collect(NativeTransferProof calldata proof, uint256 targetBlockNumber, bytes32 salt) external {
_validateBlockHeader(proof.blockHeader, targetBlockNumber);

// Verify transaction inclusion in transactions trie
Expand All @@ -83,9 +70,12 @@ contract EscrowNative is EscrowBase {
// Validate transaction succeeded (status == 1)
if (!ReceiptValidator.validateReceiptStatus(proof.receiptRlp)) revert TxFailed();

// Validate the native ETH transfer (to and value fields)
if (!ReceiptValidator.validateNativeTransfer(proof.transactionRlp, expectedRecipient, expectedAmount)) {
revert InvalidNativeTransfer();
// Extract transfer fields from the proven transaction
(address recipient, uint256 amount) = ReceiptValidator.extractNativeTransfer(proof.transactionRlp);

// Verify commitment: H(recipient, amount, salt)
if (keccak256(abi.encodePacked(recipient, amount, salt)) != commitment) {
revert CommitmentMismatch();
}

_payout();
Expand All @@ -101,19 +91,19 @@ contract EscrowNative is EscrowBase {
if (!success) revert ETHTransferFailed();
}

/// @notice Cancel and withdraw funds in a single transaction.
/// Reverts if a node has already bonded.
/// @notice Cancel and withdraw all funds in a single transaction.
/// Reverts if a bond is still active.
function cancelAndWithdraw() external {
cancellationRequest = true;
_validateWithdraw();
_handleExpiredBond();
_tryResetBondData();

uint256 withdrawableAmount = _calculateWithdrawableAmount();
uint256 withdrawableAmount = deposit;
if (withdrawableAmount == 0) revert NoWithdrawableFunds();

_clearWithdrawState();

if (withdrawableAmount == 0) revert NoWithdrawableFunds();

(bool success,) = msg.sender.call{value: withdrawableAmount}("");
if (!success) revert ETHTransferFailed();
}
Expand Down
Loading
Loading