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:
refundByRecipient(uint256 paymentID) - Requires msg.sender == payment.to (only seller can call)
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?
- Defense in depth: Minimize attack surface even for privileged roles
- Future-proofing: Protects against future code modifications
- Best practices: CEI pattern should always be followed
- Code quality signal: Demonstrates security-conscious development
- Privilege escalation: Limits damage if arbiter role is compromised
Again, HackerOne scope for Circle does not list this contract. Note that CEI is broken all over the place.
Summary
The
_executeRefundfunction violates the Checks-Effects-Interactions (CEI) pattern by performing an external token transfer before updating therefundedstate 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:
The function performs an external call before updating state, creating a potential reentrancy window where
payments[paymentID].refundedremainsfalseduring the token transfer.Limited Exploitability
Access Control Constraints:
The vulnerability's exploitability is significantly limited by access controls on the calling functions:
refundByRecipient(uint256 paymentID)- Requiresmsg.sender == payment.to(only seller can call)refundByArbiter(uint256 paymentID)- Requiresmsg.sender == arbiter(only arbiter can call)Exploit Requirements:
A reentrancy attack would require one of these scenarios:
refundToaddress could drain funds by having the arbiter repeatedly callrefundByArbiteron the same payment_executeRefundare added without proper access controlsRisk Assessment:
Proof of Concept
If the arbiter were compromised and called
refundByArbiteron a payment with a maliciousrefundTocontract: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:
Alternative: Consider using OpenZeppelin's
ReentrancyGuardon public refund functions as an additional layer of protection.Why Fix Despite Limited Exploitability?