Security review
ArcFlowLocks + ArcFlowVesting · internal review + forge test suite · Arc Mainnet
ArcFlowLocks
Verified source on Arc Explorer ↗0 critical · 0 high · 0 medium · 1 low · 5 info
- CCRinfoContract centralization risk
Owner can set the flat fee (hard-capped at 1 USDC), change the fee recipient and withdraw collected fees. There is no pause, upgrade or sweep — never any power over user escrow. Ownership transfer is two-step.
Status: Acknowledged
- AOHinfoAppend-only ownership history
locksOf(account) returns every id ever owned by an account, including locks transferred away. Callers must filter on getLock(id).owner. View-only; the UI does this and a paginated variant exists for large histories.
Status: Acknowledged
- FOTinfoFee-on-transfer tokens
Deposits measure the escrow balance delta, so a lock records what actually arrived rather than the requested amount. Tokens that rebase or charge fees on outbound transfers may deliver less on withdrawal — a property of the token, not the escrow.
Status: Acknowledged
- NVRlowArc native value rules
On Arc a native USDC send can revert at runtime (blocklisted or self-destructed recipient). Withdrawals bubble the failure, roll back state and keep the lock claimable; the owner can transfer the lock to a different address and withdraw from there.
Status: Acknowledged
- TSNinfoNon-strict block timestamps
Arc block timestamps are non-decreasing but not strictly increasing. The contract only relies on >= comparisons against unlockTime, which is unaffected.
Status: Acknowledged
- MDRinfoMaximum duration
Locks and vesting schedules are capped at 10 years from creation/extension to prevent accidental permanent escrow from mistyped timestamps.
Status: Acknowledged
ArcFlowVesting
Verified source on Arc Explorer ↗0 critical · 0 high · 0 medium · 0 low · 4 info
Shares fee, ownership, reentrancy and transfer plumbing with ArcFlowLocks via ArcFlowBase, so the CCR, FOT, NVR and TSN findings above apply identically.
- CXLinfoCancel semantics
Only the sender can cancel, and only if the schedule was created cancelable. Cancel freezes total to the vested amount at that block and refunds the remainder to the sender immediately. The recipient's vested-but-unclaimed balance stays claimable forever — cancel never pushes funds to the recipient, so a blocklisted recipient cannot block the sender's refund.
Status: Acknowledged
- BKDinfoBackdated start
start may be in the past, in which case the already-elapsed portion vests at creation. This is intentional for migrating off-chain agreements. The UI shows the immediately claimable amount before signing.
Status: Acknowledged
- RNDinfoInteger rounding
Linear vesting uses total * elapsed / duration with floor division; at most (duration - 1) wei of dust is delayed until end, never lost. Fuzz tests assert conservation: sender refund + recipient payouts == deposit.
Status: Acknowledged
- RCPinfoRecipient transfer
Only the current recipient can hand a schedule to a new address; the sender can never redirect funds. schedulesAsRecipient is append-only history and is filtered client-side on the current recipient.
Status: Acknowledged
Verified invariants
- ✓Withdrawals never exceed a lock's recorded deposit; each lock pays out at most once.
- ✓Only the current lock owner can withdraw, extend or transfer; withdrawal requires block.timestamp >= unlockTime.
- ✓unlockTime is monotonic: it can only ever increase.
- ✓Vested amount is monotonic in time and never exceeds total; recipient payouts + sender refund == deposit.
- ✓Nothing is claimable before the cliff; after cancel no further accrual happens.
- ✓withdrawFees moves only collectedFees (native fees accrued on creation) and never touches totalLocked escrow.
- ✓All fund-moving functions are reentrancy-guarded and follow checks-effects-interactions.
- ✓Stray native value sent to either contract is rejected; all deposits go through explicit create functions.
Arc-specific considerations
- USDC as gas. Native USDC has 18 decimals; the ERC-20 interface at
0x3600…0000has 6. The contracts store raw units per token and never mix the two. - 20 gwei minimum base fee. The frontend clamps
maxFeePerGasso transactions are never silently dropped. - Deterministic finality. One confirmation is final; the UI treats the first receipt as settled.