Skip to content

Reentrancy Vulnerability in _executeRefund (CEI Pattern Violation) #12

Description

@lukepuplett

Again, HackerOne scope for Circle does not list this contract. Note that CEI is broken all over the place.

Summary

The _executeRefund function violates the Checks-Effects-Interactions (CEI) pattern by performing an external token transfer before updating the refunded state variable. While access controls significantly limit exploitability, this represents a defense-in-depth concern that should be addressed.

Vulnerability Details

Location: _executeRefund (lines ~373-376)

Vulnerable Code:

function _executeRefund(uint256 paymentID, Payment memory payment) internal {
    if (payment.refunded) {
        revert PaymentRefunded(paymentID);
    }
    
    fiatToken.transfer(payment.refundTo, payment.amount);  // ❌ External call FIRST
    payments[paymentID].refunded = true;                   // ❌ State update SECOND
    
    emit Refund(paymentID, payment.refundTo, payment.amount);
}

The function performs an external call before updating state, creating a potential reentrancy window where payments[paymentID].refunded remains false during the token transfer.

Limited Exploitability

Access Control Constraints:
The vulnerability's exploitability is significantly limited by access controls on the calling functions:

  1. refundByRecipient(uint256 paymentID) - Requires msg.sender == payment.to (only seller can call)
  2. refundByArbiter(uint256 paymentID) - Requires msg.sender == arbiter (only arbiter can call)

Exploit Requirements:
A reentrancy attack would require one of these scenarios:

  • Malicious/compromised arbiter: If the arbiter's private key is stolen, an attacker controlling a malicious refundTo address could drain funds by having the arbiter repeatedly call refundByArbiter on the same payment
  • Arbiter-buyer collusion: Collusion between arbiter and buyer where arbiter is a contract with exploitable callback mechanisms
  • Future code changes: If new functions calling _executeRefund are added without proper access controls

Risk Assessment:

  • Likelihood: Low (requires privileged role compromise)
  • Impact: High (if exploited, could drain seller or arbiter balances)
  • Overall Severity: Medium (defense-in-depth concern)

Proof of Concept

If the arbiter were compromised and called refundByArbiter on a payment with a malicious refundTo contract:

contract MaliciousRefundReceiver {
    RefundProtocol public target;
    uint256 public paymentID;
    uint256 public attackCount;
    
    function tokensReceived(...) external {  // ERC777 callback
        if (attackCount < 10) {
            attackCount++;
            // This would fail due to onlyArbiter modifier
            // UNLESS arbiter itself is a vulnerable contract
            target.refundByArbiter(paymentID);
        }
    }
}

Note: This attack requires ERC777 tokens or similar with transfer callbacks. Standard USDC (ERC20) does not have callbacks, further limiting the attack surface.

Recommendation

Fix: Reorder operations to follow CEI pattern:

function _executeRefund(uint256 paymentID, Payment memory payment) internal {
    if (payment.refunded) {
        revert PaymentRefunded(paymentID);
    }
    
    payments[paymentID].refunded = true;                   // ✅ State update FIRST
    fiatToken.transfer(payment.refundTo, payment.amount);  // ✅ External call SECOND
    
    emit Refund(paymentID, payment.refundTo, payment.amount);
}

Alternative: Consider using OpenZeppelin's ReentrancyGuard on public refund functions as an additional layer of protection.

Why Fix Despite Limited Exploitability?

  1. Defense in depth: Minimize attack surface even for privileged roles
  2. Future-proofing: Protects against future code modifications
  3. Best practices: CEI pattern should always be followed
  4. Code quality signal: Demonstrates security-conscious development
  5. Privilege escalation: Limits damage if arbiter role is compromised

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions