QUIVER

Reference

Contract reference

Every public function, who can call it, and when it reverts. The signatures on this page are generated from the compiled ABI, so they cannot drift from the deployed contract.

The contract

QUIVER is one contract on Robinhood Chain (id 4663), at 0x2866D49f364a70383591a958354A89F6BDf050f2. It is an ERC721Enumerable, so the full ERC-721 surface is available alongside the streaming functions.

Generated, not transcribed

Function signatures, parameter names and types, the Arrow struct and the error list on this page are all read from lib/abi.ts, which is regenerated from the Foundry artifact with npm run abi. Only the prose is written by hand. If a documented function stopped existing, this page would fail to build rather than lie to you.

The Arrow struct

What getStreamreturns. Timestamps are absolute Unix seconds. Amounts are in the streamed token's own decimals.

sender address
Who funded the stream. Set at creation, never changes.
start uint40
When accrual begins.
cliff uint40
Nothing withdrawable before this. Equal to start means no cliff.
cancelable bool
Whether the sender can still cancel. Only ever goes true to false.
canceled bool
Whether it has been cancelled.
token address
The ERC-20 being streamed.
end uint40
When fully vested. Reset to the cancellation instant if cancelled.
deposited uint128
What the contract actually received, which is what the stream promises. Reduced to the vested amount on cancellation, so it is not a record of the original grant.
withdrawn uint128
How much has been taken out so far.

Creating

createStream(address token, address recipient, uint128 amount, uint40 start, uint40 cliff, uint40 end, bool cancelable) payable returns (uint256 id)
Escrows amount of token, mints stream NFT id to recipient, and returns the id. The fee is passed as msg.value, and anything above the current fee is refunded in the same transaction.
Parameters
  • token · The ERC-20 to stream. Fixed for the life of the stream.
  • recipient · Receives the NFT, and with it the sole right to withdraw.
  • amount · How much to escrow. The stream is credited with what the contract actually receives, which is lower for a fee-on-transfer token.
  • start · When accrual begins. May be in the past, which starts the stream partly vested.
  • cliff · Nothing is withdrawable before this. Pass `start` for no cliff.
  • end · When the stream is fully vested. Must be strictly after `start`.
  • cancelable · Whether the sender keeps the right to cancel. Can be given up later, never regained.
Who can call

Anyone, provided they have approved the contract for `amount` of `token`.

Reverts
  • ZeroAddress · `token` or `recipient` is the zero address.
  • ZeroAmount · `amount` is 0, or the contract received 0 tokens.
  • BadTimeline · `end <= start`, or `cliff < start`, or `cliff > end`.
  • InsufficientFee · `msg.value` is below the current `creationFee`.
  • Overflow · The received amount does not fit in a uint128.
  • ERC721InvalidReceiver · `recipient` has code and does not implement `onERC721Received`.
  • SafeERC20FailedOperation · The token's `transferFrom` failed or returned false.
  • EthTransferFailed · Refunding your excess `msg.value` failed, because the caller rejects ETH.
  • ReentrancyGuardReentrantCall · A token callback tried to reenter the contract.
The deposit is measured by balance difference, before and after the transfer, so a token that taxes transfers creates a stream of the net amount received. The contract never promises tokens it does not hold.

Reading a stream

getStream(uint256 id) view returns (Arrow)
The full Arrow struct for a stream.
Parameters
  • id · The stream id, which is also the NFT token id.
Who can call

Anyone.

Reverts
  • NoStream · No stream with that id was ever created.
vestedOf(uint256 id) view returns (uint128)
How much has vested so far, including tokens already withdrawn. Returns 0 before the cliff and deposited at or after end.
Parameters
  • id · The stream id.
Who can call

Anyone.

Reverts
  • NoStream · No stream with that id was ever created.
For a cancelled stream this keeps working with no special case: cancellation sets deposited to the vested amount and end to the moment of cancellation, so the same formula returns the frozen total forever.
withdrawableOf(uint256 id) view returns (uint128)
What the holder can take right now: vestedOf(id) - withdrawn. This is the amount withdrawMax sends.
Parameters
  • id · The stream id.
Who can call

Anyone.

Reverts
  • NoStream · No stream with that id was ever created.
refundableOf(uint256 id) view returns (uint128)
What would go back to the sender if they cancelled at this instant: deposited - vestedOf(id). Returns 0 for an already cancelled stream.
Parameters
  • id · The stream id.
Who can call

Anyone.

Reverts
  • NoStream · No stream with that id was ever created.
This answers a hypothetical and ignores cancelable. A non-zero result does not mean the stream can actually be cancelled.
streamsOfHolder(address holder) view returns (uint256[] ids)
The ids an address currently collects. Reflects transfers: selling a stream removes it from the seller's list.
Parameters
  • holder · The address to look up.
Who can call

Anyone, via eth_call.

Reverts
  • ERC721InvalidOwner · `holder` is the zero address.
Unbounded loop over the holder's balance. Do not call this from a contract: an address with many streams can make it exceed the block gas limit. Off-chain it is free.
streamsBySender(address sender) view returns (uint256[])
The ids an address funded, in creation order. Append-only, so cancelling removes nothing and the history stays complete.
Parameters
  • sender · The funder to look up.
Who can call

Anyone, via eth_call.

Unbounded view. Same warning as above: eth_call only, never from a contract.
sentCount(address sender) view returns (uint256)
How many streams an address has funded. The bounded way to size the list above.
Parameters
  • sender · The funder to look up.
Who can call

Anyone.

Withdrawing

withdraw(uint256 id, address to, uint128 amount)
Sends amount of the streamed token to to.
Parameters
  • id · The stream id.
  • to · Where the tokens go. Does not have to be the holder.
  • amount · How much to take. Must not exceed `withdrawableOf(id)`.
Who can call

The NFT holder, or an address they approved for this token or for all.

Reverts
  • ERC721NonexistentToken · No stream with that id was ever created.
  • ERC721InsufficientApproval · The caller is neither the holder nor approved.
  • ZeroAddress · `to` is the zero address.
  • ZeroAmount · `amount` is 0.
  • ExceedsWithdrawable · `amount` is more than is currently withdrawable.
  • SafeERC20FailedOperation · The token's `transfer` failed or returned false.
  • ReentrancyGuardReentrantCall · A token callback tried to reenter the contract.
An approved operator can withdraw to any address, not just to the holder. Approving an operator on QUIVER approves them to take the money, not only to move the NFT.
withdrawMax(uint256 id, address to) returns (uint128 amount)
Withdraws everything currently available, and returns how much that was.
Parameters
  • id · The stream id.
  • to · Where the tokens go.
Who can call

The NFT holder, or an address they approved.

Reverts
  • ZeroAmount · Nothing is withdrawable right now, for example during a cliff, or straight after another withdrawal.
  • ... · Everything `withdraw` can revert with, since this calls it.

Cancelling

cancel(uint256 id)
Stops the stream. The unvested remainder returns to the sender immediately. The vested part stays owed to the NFT holder, who can still withdraw it afterwards.
Parameters
  • id · The stream id.
Who can call

The sender only, and only while the stream is cancelable.

Reverts
  • NoStream · No stream with that id was ever created.
  • NotSender · The caller did not fund this stream. Holding the NFT does not help.
  • NotCancelable · Created irrevocable, already renounced, or already cancelled.
  • SafeERC20FailedOperation · Refunding the unvested part failed.
  • ReentrancyGuardReentrantCall · A token callback tried to reenter the contract.
Cancelling twice reverts with NotCancelable, not AlreadyCanceled, because cancel clears the flag and that check runs first. Full mechanics on Cancel and renounce.
renounceCancel(uint256 id)
Permanently gives up the sender's right to cancel, making the stream irrevocable. There is no undo.
Parameters
  • id · The stream id.
Who can call

The sender only, and only while the stream is cancelable.

Reverts
  • NoStream · No stream with that id was ever created.
  • NotSender · The caller did not fund this stream.
  • NotCancelable · Already irrevocable, already renounced, or already cancelled.

The NFT surface

QUIVER is a standard ERC721Enumerable. Everything below behaves exactly as the standard says, which is the entire point: a stream is tradable because it needs no special handling.

ownerOf(uint256 tokenId) view returns (address)
The current recipient. The only address that can withdraw, aside from operators they approved.
balanceOf(address owner) view returns (uint256)
How many streams an address holds.
tokenURI(uint256 id) view returns (string)
The stream card, as a data URI built on-chain at read time. Returns an empty string if no descriptor is set. Reverts ERC721NonexistentToken for an unminted id.
approve(address to, uint256 tokenId)
Approve one address for one stream. Cleared on transfer.
getApproved(uint256 tokenId) view returns (address)
Who is approved for this stream.
setApprovalForAll(address operator, bool approved)
Approve an operator for every stream you hold, now and later.
isApprovedForAll(address owner, address operator) view returns (bool)
Whether an operator is approved for all of an owner's streams.
transferFrom(address from, address to, uint256 tokenId)
Move a stream. Does not check the receiver can hold NFTs.
safeTransferFrom(address from, address to, uint256 tokenId)
Move a stream, checking the receiver accepts ERC-721. Prefer this.
safeTransferFrom(address from, address to, uint256 tokenId, bytes data)
Same, with a data payload passed to the receiver hook.
totalSupply() view returns (uint256)
How many streams exist in total.
tokenByIndex(uint256 index) view returns (uint256)
Enumerate all streams globally.
tokenOfOwnerByIndex(address owner, uint256 index) view returns (uint256)
Enumerate one holder's streams. What `streamsOfHolder` loops over.
supportsInterface(bytes4 interfaceId) view returns (bool)
ERC-165. True for ERC721, ERC721Enumerable and ERC721Metadata.
name() view returns (string)
The collection name.
symbol() view returns (string)
The collection symbol.

Public state

nextId() view returns (uint256)
The id the next stream will get. Starts at 1, so it is also one more than the number of streams ever created.
creationFee() view returns (uint256)
The fee, in wei, charged on the next creation. Read this rather than hardcoding it.
MAX_CREATION_FEE() view returns (uint256)
The hard cap on the fee. A compile-time constant, so nothing can raise it.
accruedFees() view returns (uint256)
Creation fees held by the contract, waiting to be swept. Accounted separately from escrowed stream tokens.
treasury() view returns (address)
Where `sweepFees` sends the accrued fees.
descriptor() view returns (IQuiverDescriptor)
The renderer that draws the stream card. Can change how a stream looks, never what it pays.
owner() view returns (address)
The protocol owner. Their reach is the fee, the treasury and the descriptor. Nothing else.

Admin

The complete owner surface. There is no admin withdraw, no pause, no upgrade, and no way for the owner to cancel a stream or touch an escrowed token.

setCreationFee(uint256 fee)
Sets the fee for future creations. Never touches an existing stream.
Parameters
  • fee · The new fee in wei. Must not exceed MAX_CREATION_FEE.
Who can call

The owner only.

Reverts
  • OwnableUnauthorizedAccount · The caller is not the owner.
  • FeeTooHigh · `fee` is above the hard cap.
setTreasury(address treasury_)
Changes where swept fees go.
Parameters
  • treasury_ · The new treasury. Cannot be the zero address.
Who can call

The owner only.

Reverts
  • OwnableUnauthorizedAccount · The caller is not the owner.
  • ZeroAddress · `treasury_` is the zero address.
setDescriptor(address descriptor_)
Swaps the renderer that draws the card. Affects art only, never balances.
Parameters
  • descriptor_ · The new renderer. The zero address disables the art, making tokenURI return an empty string.
Who can call

The owner only.

Reverts
  • OwnableUnauthorizedAccount · The caller is not the owner.
sweepFees()
Sends accrued creation fees to the treasury.
Who can call

Anyone. The destination is fixed by the owner, so this is safe to leave open.

Reverts
  • ZeroAmount · There is nothing to sweep.
  • EthTransferFailed · The treasury rejected the ETH.
  • ReentrancyGuardReentrantCall · The treasury tried to reenter the contract.
Only ever moves `accruedFees`. No path in this function reaches escrowed stream tokens.
transferOwnership(address newOwner)
Hands the owner role to another address.
Parameters
  • newOwner · The new owner. Cannot be the zero address.
Who can call

The owner only.

Reverts
  • OwnableUnauthorizedAccount · The caller is not the owner.
  • OwnableInvalidOwner · `newOwner` is the zero address.
renounceOwnership()
Leaves the contract with no owner, permanently. The fee, treasury and descriptor freeze at their current values, and accrued fees can still be swept to the last treasury set.
Who can call

The owner only.

Reverts
  • OwnableUnauthorizedAccount · The caller is not the owner.

Events

Everything the contract emits, generated from the ABI. Streams can be reconstructed from these alone if you would rather index than poll.

Approval(address indexed owner, address indexed approved, uint256 indexed tokenId)
Standard ERC-721 single-token approval.
ApprovalForAll(address indexed owner, address indexed operator, bool approved)
Standard ERC-721 operator approval.
CancelRenounced(uint256 indexed id)
The sender permanently gave up the right to cancel this stream.
CreationFeeSet(uint256 fee)
The owner changed the fee for future creations. Existing streams are unaffected.
DescriptorSet(address descriptor)
The owner swapped the card renderer.
FeesSwept(address indexed to, uint256 amount)
Accrued creation fees were sent to the treasury.
OwnershipTransferred(address indexed previousOwner, address indexed newOwner)
Standard Ownable. Emitted at deployment and on every ownership change.
StreamCanceled(uint256 indexed id, address indexed sender, uint128 refunded, uint128 vested)
A stream was stopped. `refunded` went back to the sender, `vested` stays owed to the holder.
StreamCreated(uint256 indexed id, address indexed sender, address indexed recipient, address token, uint128 amount, uint40 start, uint40 cliff, uint40 end, bool cancelable)
A stream was funded and its NFT minted. `amount` is the net amount received, which can be below the amount requested for a fee-on-transfer token.
Transfer(address indexed from, address indexed to, uint256 indexed tokenId)
Standard ERC-721. A stream changed hands, which changes who can withdraw. Minting shows as a transfer from the zero address.
TreasurySet(address treasury)
The owner changed where swept fees go.
Withdrawn(uint256 indexed id, address indexed to, uint128 amount)
Tokens were taken out of a stream, to the `to` address the caller chose.

Errors

QUIVER reverts with custom errors, never with strings. The selector is the first four bytes of the revert data, which is what you match on when decoding a failure.

QUIVER's own errors

AlreadyCanceled()
Declared, but unreachable in practice: cancel clears `cancelable`, so a second cancel hits NotCancelable first.
BadTimeline()
The schedule is impossible. Requires `start < end` and `start <= cliff <= end`.
EthTransferFailed()
An ETH transfer failed: a fee refund to a caller that rejects ETH, or a sweep to a treasury that does.
ExceedsWithdrawable()
You asked for more than is currently withdrawable from that stream.
FeeTooHigh()
A fee above MAX_CREATION_FEE was proposed, at construction or via setCreationFee.
InsufficientFee()
`msg.value` was below the current creationFee on createStream.
NoStream()
No stream with that id was ever created.
NotAContract()
NotCancelable()
The stream is not cancelable: created irrevocable, renounced, or already cancelled. Also what a second cancel reverts with.
NotSender()
Only the address that funded a stream can cancel or renounce it.
OperatorMustPayHolder()
Overflow()
The received deposit does not fit in a uint128.
RenounceDisabled()
ZeroAddress()
An address argument was the zero address, where the contract requires a real one.
ZeroAmount()
An amount was zero where that makes no sense: creating with 0, receiving 0 tokens, withdrawing 0, or sweeping an empty balance.

Inherited from OpenZeppelin

These come from ERC721, Ownable, SafeERC20 and ReentrancyGuard. They are in the ABI, so you will see them in decoded reverts.

ERC721EnumerableForbiddenBatchMint()
Batch minting is not supported by the enumerable extension. QUIVER never batch mints.
ERC721IncorrectOwner(address sender, uint256 tokenId, address owner)
The `from` address in a transfer is not the current owner.
ERC721InsufficientApproval(address operator, uint256 tokenId)
The caller is neither the holder nor approved for that stream.
ERC721InvalidApprover(address approver)
The caller cannot approve that token.
ERC721InvalidOperator(address operator)
An invalid operator was passed to an approval function.
ERC721InvalidOwner(address owner)
A zero address was passed where an owner is required, typically `balanceOf(0)`.
ERC721InvalidReceiver(address receiver)
The recipient is a contract that does not implement `onERC721Received`. Creation and safe transfers reject it so tokens cannot be stranded.
ERC721InvalidSender(address sender)
Standard ERC-721 guard on an invalid `from`, including minting from a non-zero address.
ERC721NonexistentToken(uint256 tokenId)
The token id has never been minted.
ERC721OutOfBoundsIndex(address owner, uint256 index)
An enumeration index is past the end of the list.
OwnableInvalidOwner(address owner)
Ownership was being handed to the zero address.
OwnableUnauthorizedAccount(address account)
An owner-only function was called by somebody else.
ReentrancyGuardReentrantCall()
A callback tried to reenter a guarded function.
SafeERC20FailedOperation(address token)
The ERC-20 transfer or transferFrom failed, or returned false.