Skip to content

Functions

DISCLAIMER // NFA // DYOR

This analysis is based on observations of the contract behavior. We are not smart contract security experts. This document aims to explain what the contract appears to do based on the code. It should not be considered a comprehensive security audit or financial advice. Always verify critical information independently and consult with blockchain security professionals for important decisions.

⊙ generated by robots | curated by humans

METADATA
Contract Address 0x00000000...feAaBC (etherscan)
Network Ethereum Mainnet
Analysis Date 2026-05-02

Function Selectors

SLOW (primary contract)

SELECTOR FUNCTION SIGNATURE CATEGORY
0x94eeaec9 depositTo(address,address,uint256,uint96,bytes) User
0x75f92e42 depositToWithTip(address,address,uint256,uint96,uint256,bytes) User
0xf242432a safeTransferFrom(address,address,uint256,uint256,bytes) User
0xd4fdc309 withdrawFrom(address,address,uint256,uint256) User
0x6198e339 unlock(uint256) User
0x379607f5 claim(uint256) User
0x97d15425 reverse(uint256) User
0xfcc36bc9 clawback(uint256) User
0xe9e2ef0c claimTipped(uint256) Restricted (gate-only)
0x8a0dac4a setGuardian(address) User (self)
0xfa02c4b7 approveTransfer(address,uint256) Guardian
0x47d07c4c revokeApproval(address,uint256) Guardian
0xa952a15f commitGuardian(address) User / anyone (after delay)
0xdb6c927d cancelGuardianChange(address) User / Guardian
0x2eb2c2d6 safeBatchTransferFrom(address,address,uint256[],uint256[],bytes) Disabled (always reverts)
0xac9650d8 multicall(bytes[]) User (Solady — reverts on msg.value != 0)
0xa22cb465 setApprovalForAll(address,bool) User (Solady ERC-1155)
0x06fdde03 name() View
0x95d89b41 symbol() View
0x0e89341c uri(uint256) View
0x33c34ac3 html() View
0xd8a29acf predictTransferId(address,address,uint256,uint256) View
0x2d0eae84 predictWithdrawalId(address,address,uint256,uint256) View
0xdc20c6fa decodeId(uint256) View
0xfe18969a encodeId(address,uint96) View
0x0c980180 canReverseTransfer(uint256) View
0xaade934f isGuardianApprovalNeeded(address,address,uint256,uint256) View
0xabe616b8 isWithdrawalApprovalNeeded(address,address,uint256,uint256) View
0xd40d4bc6 / 0x73bfdce5 / 0xb5f8348b getOutboundTransfers(address) / outboundTransferCount(address) / outboundTransferAt(address,uint256) View
0xe3993ee7 / 0xea712c4f / 0x8b40f4c5 getInboundTransfers(address) / inboundTransferCount(address) / inboundTransferAt(address,uint256) View
public mapping gate() / nonces(address) / guardians(address) / pendingGuardian(address) / lastGuardianChange(address) / unlockedBalances(address,uint256) / pendingTransfers(uint256) / guardianApproved(address,uint256) Auto-getter
inherited balanceOf(address,uint256) / balanceOfBatch(address[],uint256[]) / isApprovedForAll(address,address) / supportsInterface(bytes4) View (Solady ERC-1155)

Selectors above were resolved from observed transaction inputs (user functions) or computed from the verified function signatures via cast sig "<signature>" (view / pure functions).

SLOWGate (companion contract)

SELECTOR FUNCTION SIGNATURE CATEGORY
auto slow() View
auto tips(uint256) View
restricted recordTip(uint256,address,address) SLOW-only
user claim(uint256) User / Keeper
user claimMany(uint256[]) Keeper
user refundTip(uint256) Original tipper only

Summary

CATEGORY COUNT
Total externally-callable functions on SLOW 33 (including inherited Solady entry points)
User Functions on SLOW 8 (deposit, transfer, withdraw, unlock, claim, reverse, clawback, multicall)
Guardian Functions on SLOW 4 (approveTransfer, revokeApproval, cancelGuardianChange, setGuardian-as-user)
Restricted Functions on SLOW 1 (claimTipped — gate only)
View Functions on SLOW 18+ (incl. Solady inherits and public-mapping auto-getters)
Functions on SLOWGate 6 (1 SLOW-only, 3 user, 2 view)

User Functions

Function: depositTo(address token, address to, uint256 amount, uint96 delay, bytes data)

Wraps amount of token (or msg.value for native ETH) for recipient to with a delay-second timelock. Mints an ERC-1155 wrapper at face value of the deposited amount under id (token, delay). If delay == 0, the wrapper goes straight to unlockedBalances[to][id]. If delay > 0, a pendingTransfers[transferId] row is created and transferId is added to _outboundTransfers[msg.sender] and _inboundTransfers[to].

ATTRIBUTE VALUE
Selector 0x94eeaec9
Parameters token (zero for ETH), to (recipient), amount (ignored when msg.value != 0), delay (seconds), data (forwarded to _mint's onERC1155Received)
Access external, payable, permissionless, nonReentrant
Returns transferId (zero when delay == 0)
FLAG OBSERVATION
Branchless ETH vs ERC-20 dispatch on msg.value; ERC-20 path uses safeTransferFrom from Solady's SafeTransferLib
Self-deposit blocked: to != address(this). Zero-address recipient blocked: to != address(0)
Assumes vanilla ERC-20 semantics — fee-on-transfer or rebasing tokens diverge wrapper supply from reserves
Anyone can deposit to any recipient — recipient cannot opt out, and _inboundTransfers[to] grows accordingly
CONDITION REQUIREMENT
Recipient is not zero to != address(0) else InvalidRecipient()
Recipient is not the contract to != address(this) else InvalidDeposit()
If msg.value != 0: token must be zero and amount must be zero token == address(0) && amount == 0 else InvalidDeposit()
If msg.value == 0: token must be set and amount must be non-zero token != address(0) && amount != 0 else InvalidDeposit()
ERC-20 path: caller must have approved this contract for amount of token otherwise safeTransferFrom reverts
STEP ACTION
1 Validate recipient and deposit-shape preconditions
2 If ETH path: set amount = msg.value; else pull amount of token from msg.sender
3 Compute id = encodeId(token, delay)
4 Mint amount of id to to (Solady _mint, fires TransferSingle and calls onERC1155Received if to is a contract)
5 If delay > 0: derive transferId, write pendingTransfers[transferId], add to in/out sets, emit TransferPending, increment nonces[msg.sender]
6 If delay == 0: credit unlockedBalances[to][id] += amount
VARIABLE CHANGE
_balances[id][to] (ERC-1155) += amount
nonces[msg.sender] += 1 if delay > 0
pendingTransfers[transferId] written if delay > 0
_outboundTransfers[msg.sender] adds transferId if delay > 0
_inboundTransfers[to] adds transferId if delay > 0
unlockedBalances[to][id] += amount if delay == 0
CONDITION REVERT
to == address(0) InvalidRecipient()
to == address(this) InvalidDeposit()
msg.value != 0 and (token != 0 or amount != 0) InvalidDeposit()
msg.value == 0 and (token == 0 or amount == 0) InvalidDeposit()
ERC-20 transfer fails from SafeTransferLib
Reentrant call nonReentrant lock
function depositTo(address token, address to, uint256 amount, uint96 delay, bytes calldata data)
    public
    payable
    nonReentrant
    returns (uint256 transferId)
{
    require(to != address(0), InvalidRecipient());
    require(to != address(this), InvalidDeposit());

    if (msg.value != 0) {
        require(token == address(0) && amount == 0, InvalidDeposit());
        amount = msg.value;
    } else {
        require(token != address(0) && amount != 0, InvalidDeposit());
        token.safeTransferFrom(msg.sender, address(this), amount);
    }

    return _finishDeposit(token, to, amount, delay, 0, data);
}

Function: depositToWithTip(address token, address to, uint256 amount, uint96 delay, uint256 tip, bytes data)

Same as depositTo but additionally posts an ETH tip to SLOWGate. The tip pays whichever keeper lands gate.claim(transferId) after the timelock expires. delay must be non-zero and tip must be non-zero and fit in uint96 (the gate stores tips packed as uint96).

ATTRIBUTE VALUE
Selector 0x75f92e42
Parameters token, to, amount, delay, tip (ETH amount, in wei), data
Access external, payable, permissionless, nonReentrant
Returns transferId
FLAG OBSERVATION
msg.value accounting is exact: ETH branch requires msg.value == amount + tip; ERC-20 branch requires msg.value == tip
delay != 0 is enforced — tipped settlement is only meaningful for delayed transfers
tip <= type(uint96).max enforced here; gate's recordTip re-checks defensively
If to has a guardian when claimTipped runs later, settlement is blocked — the tip is then refundable to the original tipper once the underlying transfer clears via another path
CONDITION REQUIREMENT
Recipient is not zero / not this contract InvalidRecipient() / InvalidDeposit()
amount != 0 else InvalidAmount()
delay != 0 else InvalidDeposit() (no tip without a timelock)
tip != 0 && tip <= type(uint96).max else InvalidAmount()
Exact msg.value per branch amount + tip (ETH) or tip (ERC-20) — else InvalidDeposit()
STEP ACTION
1 Validate recipient and tip preconditions
2 Branch on token: ETH requires msg.value == amount + tip; ERC-20 requires msg.value == tip and safeTransferFrom of amount
3 Delegate to _finishDeposit(token, to, amount, delay, tip, data)
4 Inside _finishDeposit: same as depositTo plus call gate.recordTip{value: tip}(transferId, msg.sender, to)
VARIABLE CHANGE
Same as depositTo (always with delay > 0)
gate.tips[transferId] = Tip(uint96(tip), msg.sender)
Gate's ETH balance += tip
function depositToWithTip(
    address token,
    address to,
    uint256 amount,
    uint96 delay,
    uint256 tip,
    bytes calldata data
) public payable nonReentrant returns (uint256 transferId) {
    require(to != address(0), InvalidRecipient());
    require(to != address(this), InvalidDeposit());
    require(amount != 0, InvalidAmount());
    require(delay != 0, InvalidDeposit());
    require(tip != 0 && tip <= type(uint96).max, InvalidAmount());

    if (token == address(0)) {
        require(msg.value == amount + tip, InvalidDeposit());
    } else {
        require(msg.value == tip, InvalidDeposit());
        token.safeTransferFrom(msg.sender, address(this), amount);
    }

    return _finishDeposit(token, to, amount, delay, tip, data);
}

Function: safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data)

ERC-1155 wrapper transfer with three modes derived from (guardian on from, delay encoded in id):

  • No guardian, delay == 0: plain transfer; balance moves to unlockedBalances[to][id].
  • No guardian, delay > 0: pending transfer created; to cannot move the wrapper for delay seconds.
  • Guardian set: every transfer requires a matching guardianApproved[from][transferId] entry, regardless of delay.

Caller authorization (msg.sender == from or operator-approved) is enforced by the inherited Solady safeTransferFrom. State changes made before the inherited call roll back if it reverts.

ATTRIBUTE VALUE
Selector 0xf242432a
Parameters from, to, id, amount, data
Access external, nonReentrant, override
FLAG OBSERVATION
Op-type byte (_OP_TRANSFER = 0) is mixed into the preimage so a transfer-approval cannot be consumed by withdrawFrom
Nonce is bumped only when requiresDelayOrGuardian (delay or guardian present) — plain transfers do not waste a nonce
Transferring a delay-encoded id moves the wrapper to a fresh recipient AND restarts the timelock from block.timestamp for the new recipient — passing along an in-flight wrapper hands the recipient a fresh delay
to != gate is enforced — direct wrapper transfers to the gate are blocked
CONDITION REQUIREMENT
to != 0, to != this, to != gate else InvalidRecipient()
amount != 0 else InvalidAmount()
unlockedBalances[from][id] >= amount else safeSub underflows and reverts
If guardian is set: matching approval present guardianApproved[from][transferId] else GuardianApprovalRequired()
Caller is from or operator-approved enforced by inherited super.safeTransferFrom
STEP ACTION
1 Validate recipient and amount
2 Decrement unlockedBalances[from][id]
3 If guardian or delay: compute transferId (with _OP_TRANSFER), bump nonces[from]
4 If guardian: require approval, then delete it
5 If delay: write pendingTransfers[transferId], add to in/out sets, emit TransferPending
6 If no delay (with or without guardian): credit unlockedBalances[to][id] += amount
7 Call inherited super.safeTransferFrom (handles ERC-1155 balance move + receiver hook)
CONDITION REVERT
Bad recipient InvalidRecipient()
Zero amount InvalidAmount()
Insufficient unlocked balance underflow revert
Guardian set but no approval GuardianApprovalRequired()
Caller not from and not operator NotOwnerNorApproved (Solady)

Function: withdrawFrom(address from, address to, uint256 id, uint256 amount)

Burns amount of id from from's unlocked balance and pays the raw underlying token (or ETH) to to. If from has a guardian set, requires a matching guardianApproved[from][transferId] entry under op-type _OP_WITHDRAW. Caller authorization (msg.sender == from or operator-approved) is enforced by the inherited Solady _burn(by, from, ...).

ATTRIBUTE VALUE
Selector 0xd4fdc309
Parameters from (token holder), to (payout recipient), id (token+delay encoded), amount
Access external, nonReentrant
FLAG OBSERVATION
Op-type _OP_WITHDRAW = 1 distinguishes this approval from a transfer approval at the same (from, to, id, amount)
to != gate is enforced — raw underlying cannot be paid into the gate
Pre-burn state changes (unlocked-balance decrement, nonce bump, approval delete) all roll back together if the inherited _burn reverts on auth
CONDITION REQUIREMENT
to != 0, to != this, to != gate else InvalidRecipient()
amount != 0 else InvalidAmount()
unlockedBalances[from][id] >= amount else underflow revert
If guardian set: matching approval else GuardianApprovalRequired()
Caller is from or operator-approved else NotOwnerNorApproved
STEP ACTION
1 Decrement unlockedBalances[from][id]
2 If guardian: derive transferId with _OP_WITHDRAW, bump nonce, require approval, delete it
3 Call inherited _burn(msg.sender, from, id, amount) (this is where caller-auth is checked)
4 Pay out: if id's low 160 bits are zero, send ETH; else token.safeTransfer(to, amount)

Function: unlock(uint256 transferId)

After timelock expiry, moves a pending transfer's amount from "pending" into unlockedBalances[pt.to][pt.id]. The wrapper itself stays at pt.to. Subsequent safeTransferFrom of the same id will re-lock it for the new recipient based on the id's encoded delay.

ATTRIBUTE VALUE
Selector 0x6198e339
Parameters transferId
Access nonReentrant; gated to pt.to or any operator approved by pt.to
FLAG OBSERVATION
Operator-gated to prevent third-party griefers from frontrunning settlement and stranding the sender's clawback path or a keeper's tip on gate.claim
Removes the entry from both in/out sets and deletes pendingTransfers[transferId]
CONDITION REQUIREMENT
Transfer exists pt.timestamp != 0 else TransferDoesNotExist()
Timelock has expired block.timestamp >= pt.timestamp + (pt.id >> 160) else TimelockNotExpired()
Caller is pt.to or operator-approved by pt.to else Unauthorized()
STEP ACTION
1 Credit unlockedBalances[pt.to][pt.id] += pt.amount
2 Remove transferId from _outboundTransfers[pt.from] and _inboundTransfers[pt.to]
3 Delete pendingTransfers[transferId]; emit Unlocked(pt.to, pt.id, pt.amount)

Function: claim(uint256 transferId)

Auto-settle path: after timelock expiry, burns the wrapper from pt.to and pays the raw underlying directly to pt.to. Skips the unlocked-balance step that unlock + withdrawFrom would take. Reverts if pt.to has a guardian set — guarded recipients must use the wrapper-then-withdraw path so the raw exit inherits guardian gating.

ATTRIBUTE VALUE
Selector 0x379607f5
Parameters transferId
Access nonReentrant; gated to pt.to or any operator approved by pt.to
FLAG OBSERVATION
Guardian-gated: guardians[pt.to] == address(0) required, else ClaimBlockedByGuardian()
Internal _doClaim uses _burn(address(0), ...) which skips Solady's NotOwnerNorApproved check — every external caller must enforce its own auth (this function does, and so does claimTipped)
Code comment notes: any future caller of _doClaim MUST enforce its own auth

Function: claimTipped(uint256 transferId)

Sender-sponsored claim path. Skips the operator-approval check and is callable only by the gate. The gate invokes this only from _claimAndPay for transferIds that carry a posted tip. Guardian veto on pt.to still applies.

ATTRIBUTE VALUE
Selector 0xe9e2ef0c
Access nonReentrant; gate-only
CONDITION REVERT
Caller is not the gate Unauthorized()
Transfer does not exist TransferDoesNotExist()
Timelock has not expired TimelockNotExpired()
Recipient has a guardian ClaimBlockedByGuardian()

Function: reverse(uint256 transferId)

Cancels a pending transfer before its timelock expires. Returns the wrapper to pt.from and credits unlockedBalances[pt.from][pt.id]. The move uses _safeTransfer, which calls onERC1155Received on pt.from per ERC-1155 spec — contract senders that didn't implement IERC1155Receiver are not reverse-eligible.

ATTRIBUTE VALUE
Selector 0x97d15425
Access nonReentrant; gated to pt.from or operator-approved by pt.from
CONDITION REQUIREMENT
Transfer exists pt.timestamp != 0
Timelock has not yet expired block.timestamp < pt.timestamp + (id >> 160) else TimelockExpired()
Caller is pt.from or operator else Unauthorized()

Function: clawback(uint256 transferId)

Sender recovery for unsettled transfers (e.g. dead/lost recipient). Available 30 days past timelock expiry. Returns the wrapper to pt.from and credits their unlocked balance. Like reverse, _safeTransfer invokes onERC1155Received on pt.from, so contract senders need to implement the receiver interface to be eligible.

ATTRIBUTE VALUE
Selector 0xfcc36bc9
Access nonReentrant; gated to pt.from or operator-approved by pt.from
CONDITION REQUIREMENT
Transfer exists pt.timestamp != 0
At least delay + 30 days past deposit block.timestamp >= pt.timestamp + (id >> 160) + 30 days else ClawbackNotReady()
Caller is pt.from or operator else Unauthorized()
FLAG OBSERVATION
Wrapper-route only — once unlock or claim runs, settlement to pt.to is final and clawback is no longer reachable
Subsequent raw exit by pt.from goes through withdrawFrom and inherits any guardian gating that pt.from has set

Guardian Functions

Function: setGuardian(address newGuardian)

Arms or rotates the caller's guardian. First-time set (or post-removal set) is immediate. Rotating an already-active guardian stages a pendingGuardian entry with effectiveAt = now + 1 day and emits GuardianChangeProposed. During the window, the user or current guardian can cancel via cancelGuardianChange. Re-proposing the current guardian during the window cancels the in-flight rotation. After the window, only commitGuardian is valid.

ATTRIBUTE VALUE
Selector 0x8a0dac4a
Access external, nonReentrant; affects only the caller's row
FLAG OBSERVATION
Self-guardian blocked (newGuardian != msg.sender) — a stolen key could approve its own outflows
First-time set bumps lastGuardianChange[msg.sender] to invalidate any dangling pre-set approvals
After the rotation window, the only ways to abort are: propose a different guardian (restarts the window) and then cancel during the new window — setGuardian(currentGuardian) no longer cancels late
Setting newGuardian = address(0) removes the current guardian via the rotation path; this gives the current guardian a 1-day veto window over their own removal
function setGuardian(address newGuardian) public nonReentrant {
    require(newGuardian != msg.sender, InvalidGuardian());
    if (newGuardian == guardians[msg.sender]) {
        uint256 effectiveAt = pendingGuardian[msg.sender].effectiveAt;
        if (effectiveAt != 0 && block.timestamp < effectiveAt) {
            delete pendingGuardian[msg.sender];
            emit GuardianChangeCanceled(msg.sender);
        }
        return;
    }
    if (guardians[msg.sender] == address(0)) {
        delete pendingGuardian[msg.sender];
        lastGuardianChange[msg.sender] = block.timestamp;
        emit GuardianSet(msg.sender, guardians[msg.sender] = newGuardian);
    } else {
        unchecked {
            uint256 effectiveAt = block.timestamp + _GUARDIAN_CHANGE_DELAY;
            pendingGuardian[msg.sender] = PendingGuardian(newGuardian, uint96(effectiveAt));
            emit GuardianChangeProposed(msg.sender, newGuardian, effectiveAt);
        }
    }
}

Function: commitGuardian(address user)

Permissionless poke that applies a staged guardian rotation once effectiveAt has passed. Bumps lastGuardianChange[user], which atomically invalidates any dangling guardian approvals bound to the previous preimage.

ATTRIBUTE VALUE
Selector 0xa952a15f
Access external, anyone
CONDITION REVERT
No rotation pending NoGuardianChangePending()
block.timestamp < effectiveAt GuardianChangeNotReady()

Function: cancelGuardianChange(address user)

Vetoes a pending guardian change during the 1-day window. Callable by user or by guardians[user]. After the window, only commitGuardian is valid.

ATTRIBUTE VALUE
Selector 0xdb6c927d
Access external, restricted to user or current guardian
FLAG OBSERVATION
Guardian-side cancel is the protection: defeats a stolen key proposing setGuardian(attacker)
A hostile guardian can veto every rotation indefinitely. The author's framing in code comments is "appoint a guardian only if you trust them" — this is the intentional shape of co-sign

Function: approveTransfer(address from, uint256 transferId)

Pre-approves a specific transferId for from. Callable only by guardians[from]. The transferId must match the preimage keccak256(from, to, id, amount, nonces[from], lastGuardianChange[from], op-type) for the next safeTransferFrom (op-type 0) or withdrawFrom (op-type 1) the user intends to execute. Use predictTransferId for transfer approvals and predictWithdrawalId for withdraw approvals — the preimages differ.

ATTRIBUTE VALUE
Selector 0xfa02c4b7
Access external, guardian-only
FLAG OBSERVATION
The on-chain op-split prevents cross-op consumption (a transfer approval cannot satisfy a withdraw and vice versa)
The op-split does not prevent malicious approval of the wrong op — guardians must still verify intent off-chain. The contract trusts the guardian to validate destination, amount, and op-type before signing

Function: revokeApproval(address from, uint256 transferId)

Retracts a previously granted approval. Callable only by guardians[from]. Lets the guardian undo a single mistaken approval without rotating (which would invalidate every dangling approval). Idempotent — revoking a clear slot is a no-op.

ATTRIBUTE VALUE
Selector 0x47d07c4c
Access external, guardian-only

View Functions

Function: predictTransferId(from, to, id, amount)

Returns the transferId for the next guardian-gated or delayed safeTransferFrom by from at the current nonces[from] and lastGuardianChange[from]. Uses op-type _OP_TRANSFER. Plain transfers (no delay, no guardian) consume no id — this is the handle for guardian co-sign of wrapper transfers.

Function: predictWithdrawalId(from, to, id, amount)

Same shape but with op-type _OP_WITHDRAW. The op-type byte separates withdraw approvals from transfer approvals at the same (from, to, id, amount).

Function: decodeId(uint256 id) → (address token, uint256 delay)

Splits the composite id back into (token, delay). Pure.

Function: encodeId(address token, uint96 delay) → uint256 id

Composes the id from (token, delay). Pure.

Function: canReverseTransfer(uint256 transferId) → (bool canReverse, bytes4 reason)

Returns (true, "") if the transfer exists and the timelock has not expired. Otherwise returns (false, selector) with one of TransferDoesNotExist.selector or TimelockExpired.selector.

Function: isGuardianApprovalNeeded(user, to, id, amount) → bool

true iff guardians[user] != 0 and the precomputed transfer-op transferId is not yet approved.

Function: isWithdrawalApprovalNeeded(user, to, id, amount) → bool

true iff guardians[user] != 0 and the precomputed withdraw-op transferId is not yet approved.

Function: getOutboundTransfers(user) / getInboundTransfers(user)

Returns the full set of currently-pending transferIds from the user's outbound or inbound set. The author calls out in code comments that both arrays are unbounded and grow with the set size (_inboundTransfers can be expanded by anyone via dust deposits). On-chain consumers should paginate via *Count + *At(i).

Function: outboundTransferAt(user, i) / inboundTransferAt(user, i)

Positional read of a transferId from the corresponding set. EnumerableSetLib swaps with the last element on remove, so positional reads are not stable across settlement. Indexers should snapshot via the array getters or react to events.

Function: outboundTransferCount(user) / inboundTransferCount(user)

Pagination bound for the corresponding set.

Function: html()

Returns the full SLOW dapp HTML reassembled from htmlChunk1 and htmlChunk2 via Solady's SSTORE2.read. Pure metadata — does not affect consensus or fund handling.

Function: name() / symbol() / uri(id)

name() returns "SLOW", symbol() returns "SLOW", uri(id) returns a base64-encoded JSON metadata blob with embedded SVG. uri reads name/symbol of the underlying token via Solady's MetadataReaderLib and clips/escapes them with _utf8Trim and LibString.escapeJSON / escapeHTML to keep the JSON and SVG payloads valid for strict marketplace parsers.


Disabled Function

Function: safeBatchTransferFrom(...)

Always reverts with BatchTransferDisabled(). ERC-1155 batch semantics are not supported. The author's stated rationale in the contract NatSpec is to avoid spamming the inbound/outbound sets via batched zero-amount delayed sends.


SLOWGate Functions

Function: recordTip(uint256 transferId, address sender, address to)

Records a tip posted by SLOW during a depositToWithTip call. Stores tips[transferId] = Tip(uint96(msg.value), sender) and emits TipPosted.

ATTRIBUTE VALUE
Access external, payable, SLOW-only (msg.sender == address(slow))
FLAG OBSERVATION
Defense-in-depth bound on msg.value: although depositToWithTip already enforces tip <= type(uint96).max, the gate re-checks here so a future caller bypassing the upstream check cannot silently lose value via the uint96 cast

Function: claim(uint256 transferId)

Single-transfer settlement entry point. Delegates to _claimAndPay. If a tip is attached, pays it to msg.sender and routes through slow.claimTipped. If no tip, routes through slow.claim — which requires pt.to to have approved the gate via setApprovalForAll (the auto-claim opt-in).


Function: claimMany(uint256[] transferIds)

Atomic batch settlement. Loops over _claimAndPay for each id. The whole call reverts on the first failure — keepers must filter ids off-chain for expiry, no-guardian status, and pending presence.


Function: refundTip(uint256 transferId)

Recovers an unclaimed tip after the underlying transfer cleared by a non-gate path (unlock, reverse, clawback, or recipient-direct claim). Callable only by the original tip sender. Reverts while the transfer is still pending (the gate reads slow.pendingTransfers(transferId) and requires ts == 0).

ATTRIBUTE VALUE
Access external, restricted to original tipper
CONDITION REVERT
Pending transfer still exists TipStillPending()
No tip recorded for this id NoTip()
Caller is not the original tipper Unauthorized()

Lifecycle Diagram — A Pending Transfer

sequenceDiagram
    autonumber
    actor Sender
    participant SLOW
    participant Gate
    participant Token
    actor Recipient
    actor Keeper

    Sender->>SLOW: depositToWithTip(token, Recipient, amt, delay, tip, data)
    SLOW->>Token: safeTransferFrom(Sender, SLOW, amt) — ERC-20 only
    SLOW->>SLOW: mint wrapper id to Recipient
    SLOW->>SLOW: write pendingTransfers entry
    SLOW->>Gate: recordTip(transferId, Sender, Recipient) with tip value
    SLOW-->>Sender: emit TransferPending
    Note over SLOW,Recipient: timelock window — Sender may reverse, Recipient waits

    alt Sender reverses before expiry
        Sender->>SLOW: reverse(transferId)
        SLOW->>SLOW: credit unlocked balance to Sender, delete pending
        SLOW-->>Sender: emit TransferReversed
        Sender->>Gate: refundTip(transferId)
        Gate-->>Sender: tip refund
    else Recipient settles directly after expiry
        Recipient->>SLOW: claim(transferId) or unlock + withdrawFrom
        SLOW->>Token: safeTransfer(Recipient, amt)
        SLOW-->>Recipient: emit TransferClaimed
        Sender->>Gate: refundTip(transferId)
        Gate-->>Sender: tip refund
    else Keeper settles via gate after expiry
        Keeper->>Gate: claim(transferId)
        Gate->>SLOW: claimTipped(transferId)
        SLOW->>Token: safeTransfer(Recipient, amt)
        Gate->>Keeper: tip payout
        SLOW-->>Recipient: emit TransferClaimed
        Gate-->>Keeper: emit TipPaid
    else Recipient never settles after 30 day grace
        Sender->>SLOW: clawback(transferId)
        SLOW->>SLOW: credit unlocked balance to Sender, delete pending
        SLOW-->>Sender: emit TransferClawedBack
        Sender->>Gate: refundTip(transferId)
        Gate-->>Sender: tip refund
    end

Decision Tree — setGuardian(newGuardian)

The same entry point covers four distinct behaviors depending on the current state of guardians[msg.sender] and pendingGuardian[msg.sender]. The branches below are the load-bearing observation: a single call shape (setGuardian) is doing first-time set, removal-staging, rotation-staging, AND in-window cancel.

flowchart TD
    Start(["msg.sender calls setGuardian"])

    Start --> Self{"newGuardian == msg.sender?"}
    Self -->|yes| RevertSelf["revert InvalidGuardian"]

    Self -->|no| Same{"newGuardian == current guardian?"}

    Same -->|yes| InWindow{"rotation pending<br/>and still before effectiveAt?"}
    InWindow -->|yes| CancelInWindow["delete pendingGuardian<br/>emit GuardianChangeCanceled<br/>return"]
    InWindow -->|no| Noop["return — no-op"]

    Same -->|no| HasGuardian{"current guardian set?"}

    HasGuardian -->|no| Immediate["guardians := newGuardian<br/>lastGuardianChange := now<br/>emit GuardianSet<br/>takes effect immediately"]

    HasGuardian -->|yes| Stage["stage pendingGuardian<br/>effectiveAt := now + 1 day<br/>emit GuardianChangeProposed<br/>commit allowed after delay"]

    Stage -.->|veto path during window| VetoNote["user OR current guardian<br/>can cancelGuardianChange<br/>before effectiveAt"]
    Stage -.->|hostile-takeover defense| TakeoverNote["stolen key cannot remove<br/>the guardian instantly —<br/>guardian sees the proposal<br/>and cancels during the window"]

    style RevertSelf fill:#ffe1e1
    style CancelInWindow fill:#fff4e1
    style Immediate fill:#e1f5ff
    style Stage fill:#e1f5ff

Two non-obvious consequences fall out of this shape:

  • Setting newGuardian = address(0) (removal) takes the rotation path while a guardian is active, so removal also costs a 1-day window — and the current guardian can veto their own removal during that window. That asymmetry is what the contract trades for protection against stolen-key removals.
  • Once block.timestamp >= effectiveAt, the in-window cancel branch (Same → InWindow → yes) is unreachable — setGuardian(currentGuardian) becomes the no-op path, and the only valid action on the pending entry is commitGuardian (callable by anyone). To abort late, the user must propose a different guardian to restart the window, then cancel during the new window.