Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8313d62
test: add MockERC20 and MockVerifier
natalya-bbr Dec 3, 2025
92bfa40
test: SafeBaseEscrowV1 initialization and escrow creation
natalya-bbr Dec 3, 2025
95b908c
test: add comprehensive tests for RulesEngine, Registry, Executor, an…
natalya-bbr Dec 3, 2025
520d832
ci: improve GitHub Actions workflows
natalya-bbr Dec 3, 2025
8f5934e
fix: replace transfer() with call() for UUPS proxy compatibility
natalya-bbr Dec 3, 2025
52c6f04
fix: add executeWithdrawal calls in release and refund functions
natalya-bbr Dec 3, 2025
2ad93e8
ci: fix gas snapshot step for first run
natalya-bbr Dec 3, 2025
732da59
feat: add UUPS upgrade workflow and script
natalya-bbr Dec 4, 2025
53f47c1
chore: update SafeBaseEscrow implementation addresses after upgrade
natalya-bbr Dec 4, 2025
6013f40
fix: correct __Ownable_init for OpenZeppelin v4.9.6 compatibility
natalya-bbr Dec 11, 2025
ad64fd4
test: add Disputed → Released transition tests
natalya-bbr Dec 11, 2025
7da0046
test: add RulesEngine integration test suite
natalya-bbr Dec 11, 2025
f64b600
docs: add escrow lifecycle specification
natalya-bbr Dec 11, 2025
728162c
fix: use correct __Ownable_init signature for OpenZeppelin v5.5
natalya-bbr Dec 11, 2025
ac44c63
fix: correct test order in testDisputedToReleasedRequiresMediator
natalya-bbr Dec 11, 2025
0c123f0
fix: enforce mediator-only actions in Disputed state
natalya-bbr Dec 11, 2025
aeece92
chore: update deployment addresses after contract upgrades
natalya-bbr Dec 11, 2025
647130c
Merge branch
natalya-bbr Dec 11, 2025
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
4 changes: 2 additions & 2 deletions deployments/8453.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@
},
"RulesEngine": {
"proxy": "0x7bFA481f050AC09d676A7Ba61397b3f4dac6E558",
"implementation": "0xb25dcb1488247Fa0e29e6813fb9BAb630E375517"
"implementation": "0xB49e7b4cCB76B3aE9439798eb980434CBCF8c428"
},
"Registry": {
"proxy": "0x273930106653461A2F4f33Ea2821652283dcAE11",
"implementation": "0xC1E06BfBBe9b812BFc5feFBe4efDFb7B9A4E64cB"
},
"SafeBaseEscrow": {
"proxy": "0x1B079e9519CF110b491a231d7AA67c9a597F13B2",
"implementation": "0x57741EE5bAc991D43Cf71207781fCB0eE4b9e9a8"
"implementation": "0xA1e13a0E7E54bC71ee4173D74773b455A86816aB"
},
"Executor": {
"proxy": "0xdBa335d18751944b46f205F32F03Fa4F1BEf1a94",
Expand Down
4 changes: 2 additions & 2 deletions deployments/84532.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@
},
"RulesEngine": {
"proxy": "0xDb1855c6C8ADd51eE4B7e132173cA9833B1DAf07",
"implementation": "0xf3377563303A664141c98643f98a9918456f2216"
"implementation": "0xFA439194fe9B624AD51f7ecccf9FbcdB4350Bc70"
},
"Registry": {
"proxy": "0x57741EE5bAc991D43Cf71207781fCB0eE4b9e9a8",
"implementation": "0x509014Ac3d57ee08B2A47639aCDeEaE1B700960f"
},
"SafeBaseEscrow": {
"proxy": "0xA1e13a0E7E54bC71ee4173D74773b455A86816aB",
"implementation": "0xF2a7fbffD5760721C99104A7C1f500797F3D1314"
"implementation": "0x682026827839A367252Ec80a0bbaaA47AFA3d870"
},
"Executor": {
"proxy": "0xB49e7b4cCB76B3aE9439798eb980434CBCF8c428",
Expand Down
245 changes: 245 additions & 0 deletions docs/escrow-lifecycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
# SafeBase Escrow Lifecycle Specification

## Overview

SafeBase escrow system is a production-grade, modular escrow and conditional payment layer for Base L2. The system consists of two core components:

- **SafeBaseEscrowV1**: State machine managing escrow lifecycle and fund custody
- **RulesEngineV1**: Programmable condition evaluation for release/refund decisions

## State Machine

### States (6 total)

1. **Created** - Initial state after escrow creation
2. **Funded** - Funds deposited (via direct funding or Base Pay)
3. **Released** - Funds released to seller (terminal state)
4. **Refunded** - Funds refunded to buyer (terminal state)
5. **Disputed** - Dispute raised, requires mediator intervention
6. **Cancelled** - Escrow cancelled before funding (terminal state)

### State Transition Diagram

```
Created ──fundEscrow()──────────> Funded ──releaseToSeller()──> Released ✓
│ │
│ ├──refundToBuyer()────────> Refunded ✓
│ │
└──cancelEscrow()──> Cancelled ✓ └──disputeEscrow()────────> Disputed
├──releaseToSeller()──> Released ✓
└──refundToBuyer()────> Refunded ✓
```

### Allowed Transitions

| From | To | Trigger | Authorization |
|------------|------------|----------------------|--------------------------------------------------|
| Created | Funded | fundEscrow() | Buyer only |
| Created | Funded | fundEscrowWithBasePay() | Anyone (off-chain verified) |
| Created | Cancelled | cancelEscrow() | Buyer only |
| Funded | Released | releaseToSeller() | Buyer (with approval) OR Mediator |
| Funded | Refunded | refundToBuyer() | Mediator OR Anyone after deadline |
| Funded | Disputed | disputeEscrow() | Buyer OR Seller (requires mediator set) |
| Disputed | Released | releaseToSeller() | Mediator only |
| Disputed | Refunded | refundToBuyer() | Mediator only |

**Terminal states**: Released, Refunded, Cancelled - no transitions out.

## Roles

### Buyer
- Creates escrow
- Funds escrow
- Can approve release (buyerApproved flag)
- Can raise dispute
- Receives refund

### Seller
- Receives funds on release
- Can approve delivery (sellerApproved flag - future use)
- Can raise dispute

### Mediator (optional)
- Can override any approval requirements
- Can release or refund from any Funded/Disputed state
- Required for dispute resolution
- Escrows without mediator cannot be disputed

## Rules Engine Integration

### Without Rules Engine (ruleSetId = 0)
Default behavior:
- **Release**: Requires buyer approval (buyerApproved) OR mediator override
- **Refund**: Requires mediator action OR deadline expiration

### With Rules Engine (ruleSetId > 0)
Delegates decision logic to RulesEngineV1:
- **canRelease()**: Evaluates approval requirements, mediator override, external verifiers
- **canRefund()**: Evaluates deadline, mediator override, auto-refund rules

### Rule Set Structure
```solidity
struct RuleSet {
bool requireBuyerApproval; // Buyer must approve before release
bool requireSellerApproval; // Seller must approve (future)
bool autoRefundAfterDeadline; // Auto-refund when deadline expires
bool autoReleaseOnFullApproval; // Auto-release when both parties approve
bool mediatorOverrideEnabled; // Mediator can bypass all rules
bool externalVerifierEnabled; // Use external contract for verification
address externalVerifier; // External verifier contract address
}
```

## Invariants

### State Invariants
1. **Monotonic state progression**: Once in terminal state (Released/Refunded/Cancelled), state cannot change
2. **Single terminal state**: An escrow can only reach one terminal state
3. **Dispute requires mediator**: Cannot transition to Disputed if mediator == address(0)
4. **Funding immutability**: Once Funded, amount and parties cannot change

### Financial Invariants
1. **Conservation**: Funds held in Treasury equal sum of all Funded/Disputed escrows
2. **Single release**: Each escrow can only trigger one withdrawal (release OR refund, never both)
3. **Amount consistency**: Withdrawal amount always equals escrow.amount

### Authorization Invariants
1. **Buyer exclusivity**: Only buyer can fund and cancel
2. **Mediator authority**: Mediator can always release/refund from Funded/Disputed states
3. **Approval requirement**: Non-mediator releases require buyerApproved (unless rules override)

### Temporal Invariants
1. **Deadline in future**: createEscrow requires deadline > block.timestamp
2. **Auto-refund**: After deadline, anyone can trigger refund (if no mediator)
3. **No deadline bypass**: Release does not check deadline (allows late fulfillment)

## Edge Cases

### Race Conditions
1. **Approve + Deadline**: Buyer approves just as deadline expires
- **Result**: Both release and refund are valid; first transaction wins

2. **Dispute + Release**: Buyer disputes while mediator releases
- **Result**: Dispute fails (state already Released) OR Release fails (state Disputed)

3. **Dual approval**: Buyer and seller approve simultaneously
- **Result**: Both approvals succeed; state unchanged until release called

### Failure Modes
1. **Missing verifier**: If externalVerifierEnabled but verifier == address(0)
- **Result**: canRelease() returns false, blocking release

2. **Treasury withdrawal fails**: Network congestion or recipient revert
- **Result**: State changes to Released/Refunded but withdrawal reverts entire transaction

3. **Mediator unavailable**: Escrow disputed but mediator address compromised
- **Result**: Funds locked until mediator acts; no fallback mechanism

### Boundary Conditions
1. **Zero deadline**: Prevented by createEscrow validation
2. **Zero amount**: Prevented by createEscrow validation
3. **Missing seller**: Prevented by createEscrow validation
4. **Missing mediator**: Allowed; disputes disabled for this escrow

## Integration Patterns

### Standard Escrow Flow
```solidity
// 1. Create escrow
uint256 escrowId = escrow.createEscrow(
seller,
mediator,
address(0), // ETH
1 ether,
block.timestamp + 7 days,
ruleSetId
);

// 2. Buyer funds
escrow.fundEscrow{value: 1 ether}(escrowId);

// 3. Buyer approves after delivery
escrow.approveBuyer(escrowId);

// 4. Release to seller
escrow.releaseToSeller(escrowId);
```

### Base Pay Integration
```solidity
// Off-chain: User pays via Base Pay
// On-chain: Verifier calls fundEscrowWithBasePay
escrow.fundEscrowWithBasePay(escrowId, paymentId);
```

### Dispute Resolution
```solidity
// Buyer raises dispute
escrow.disputeEscrow(escrowId);

// Mediator investigates and decides
// Option A: Release to seller
escrow.releaseToSeller(escrowId);

// Option B: Refund to buyer
escrow.refundToBuyer(escrowId);
```

## Upgrade Considerations

### Storage Layout
SafeBaseEscrowV1 uses UUPS proxy pattern:
- `EscrowData` struct can be extended (append-only)
- New fields added in Block 1: `ruleSetId`
- Gap: `uint256[50] __gap` reserved for future storage

### Breaking Changes from V0 (if exists)
1. `createEscrow()` now requires `ruleSetId` parameter
2. `releaseToSeller()` now accepts Disputed state
3. RulesEngine integration changes authorization logic

### Migration Strategy
- Existing escrows (without ruleSetId) continue with legacy logic
- New escrows can use Rules Engine by setting ruleSetId > 0
- No state migration required; backward compatible

## Security Model

### Trust Assumptions
1. **Mediator honesty**: Mediators have full control over disputed escrows
2. **Treasury security**: Multi-sig treasury protects all funds
3. **Rules immutability**: Once created, RuleSets cannot be modified (future: add updateRuleSet)

### Attack Vectors
1. **Mediator collusion**: Mediator + Buyer/Seller collude to steal funds
- **Mitigation**: Reputation system, EAS attestations (future blocks)

2. **Denial of service**: Attacker creates many escrows to bloat state
- **Mitigation**: Registry indexing, off-chain filtering (Block 6)

3. **Reentrancy**: Withdrawal triggers external call to recipient
- **Mitigation**: ReentrancyGuard on releaseToSeller/refundToBuyer

### Access Control
- **Owner**: Can set rulesEngine, registry, pause contract
- **Buyer**: Can create, fund, approve, dispute, cancel
- **Seller**: Can approve, dispute
- **Mediator**: Can release, refund from Funded/Disputed
- **Anyone**: Can refund after deadline (if no mediator)

## Future Enhancements (Beyond Block 1)

1. **Partial releases**: Split payments for milestone-based escrows
2. **Multi-token support**: ERC20 token escrows (currently ETH only)
3. **Time-locked releases**: Automatic release after deadline + approval
4. **Appeal mechanism**: Secondary mediator for disputed cases
5. **Escrow templates**: Pre-configured rule sets for common use cases
6. **Event-driven automation**: Executor integration for auto-release/refund

---

**Version**: 1.0 (Block 1 - Lifecycle Hardening)
**Last Updated**: 2025-12-11
**Maintainer**: SafeBase Core Team
64 changes: 58 additions & 6 deletions src/escrow/SafeBaseEscrowV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Own
import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import {Treasury} from "../Treasury.sol";

interface IRulesEngine {
function canRelease(
uint256 ruleSetId,
bool buyerApproved,
bool sellerApproved,
bool isMediatorOverride,
uint256 escrowId,
bytes calldata verifierData
) external view returns (bool);

function canRefund(
uint256 ruleSetId,
uint256 deadline,
bool isMediatorOverride
) external view returns (bool);
}

contract SafeBaseEscrowV1 is
Initializable,
UUPSUpgradeable,
Expand All @@ -28,6 +45,7 @@ contract SafeBaseEscrowV1 is
bool sellerApproved;
bytes32 paymentId;
uint256 createdAt;
uint256 ruleSetId;
}

Treasury public treasury;
Expand Down Expand Up @@ -94,7 +112,8 @@ contract SafeBaseEscrowV1 is
address _mediator,
address _token,
uint256 _amount,
uint256 _deadline
uint256 _deadline,
uint256 _ruleSetId
) external whenNotPaused returns (uint256) {
if (_seller == address(0)) revert InvalidAddress();
if (_amount == 0) revert InvalidAmount();
Expand All @@ -113,7 +132,8 @@ contract SafeBaseEscrowV1 is
buyerApproved: false,
sellerApproved: false,
paymentId: bytes32(0),
createdAt: block.timestamp
createdAt: block.timestamp,
ruleSetId: _ruleSetId
});

emit EscrowCreated(escrowId, msg.sender, _seller, _token, _amount, _deadline);
Expand Down Expand Up @@ -170,13 +190,32 @@ contract SafeBaseEscrowV1 is

function releaseToSeller(uint256 _escrowId) external nonReentrant whenNotPaused {
EscrowData storage escrow = escrows[_escrowId];
if (escrow.state != EscrowState.Funded) revert InvalidState();
if (escrow.state != EscrowState.Funded && escrow.state != EscrowState.Disputed) {
revert InvalidState();
}

bool isMediator = msg.sender == escrow.mediator && escrow.mediator != address(0);
bool isBuyer = msg.sender == escrow.buyer;

if (!isMediator && !isBuyer) revert Unauthorized();
if (!isMediator && !escrow.buyerApproved) revert Unauthorized();

if (escrow.state == EscrowState.Disputed && !isMediator) {
revert Unauthorized();
}

if (rulesEngine != address(0) && escrow.ruleSetId != 0) {
bool canRelease = IRulesEngine(rulesEngine).canRelease(
escrow.ruleSetId,
escrow.buyerApproved,
escrow.sellerApproved,
isMediator,
_escrowId,
""
);
if (!canRelease) revert Unauthorized();
} else {
if (!isMediator && !escrow.buyerApproved) revert Unauthorized();
}

escrow.state = EscrowState.Released;

Expand All @@ -192,9 +231,22 @@ contract SafeBaseEscrowV1 is
if (escrow.state != EscrowState.Funded && escrow.state != EscrowState.Disputed) revert InvalidState();

bool isMediator = msg.sender == escrow.mediator && escrow.mediator != address(0);
bool canRefund = isMediator || block.timestamp > escrow.deadline;

if (!canRefund) revert Unauthorized();
if (escrow.state == EscrowState.Disputed && !isMediator) {
revert Unauthorized();
}

if (rulesEngine != address(0) && escrow.ruleSetId != 0) {
bool canRefund = IRulesEngine(rulesEngine).canRefund(
escrow.ruleSetId,
escrow.deadline,
isMediator
);
if (!canRefund) revert Unauthorized();
} else {
bool canRefund = isMediator || block.timestamp > escrow.deadline;
if (!canRefund) revert Unauthorized();
}

escrow.state = EscrowState.Refunded;

Expand Down
Loading