Sitowise
Payouts

Withdrawing

Withdrawing is a single transaction, sent by you from the wallet that owns the node. There is nothing to prepare, nothing that expires, and no server in the path. The contract always sends the whole balance.

One call, sent by you

There are two functions and you call one of them directly. Neither takes an amount, because both take everything the node holds.

SitowiseFactory
function withdraw(uint256 id, address to) external;
function withdrawAll(address to) external returns (uint256 amount);
  • withdraw(id, to) empties one node.
  • withdrawAll(to) sweeps every node the calling wallet owns in one transaction, and pays the combined total in one transfer.

No signature, no approval step, no allowance, no second transaction, no API call. A balance is already real ETH held by the factory against your node id, so the only question at withdrawal time is who is asking, and msg.sender answers it. Where that ETH came from is on Balances.

What the contract does

SitowiseFactory.withdraw
function withdraw(uint256 id, address to) external nonReentrant {
    Node storage node = _node[id];
    if (node.owner != msg.sender) revert NotNodeOwner();
    if (to == address(0)) revert BadInput();

    uint256 amount = node.balance;
    if (amount == 0) revert NothingToWithdraw();

    node.balance = 0;
    node.totalWithdrawn += uint128(amount);
    outstanding -= amount;
    totalWithdrawn += amount;

    emit Withdrawn(id, to, amount);
    (bool ok,) = payable(to).call{value: amount}("");
    if (!ok) revert TransferFailed();
}

Three checks and one transfer, in an order chosen deliberately.

  • Ownership is checked against msg.sender, and against nothing else. There is no second party whose permission is needed and none who could stand in for you.
  • The destination is checked before any storage is touched, so a zero address costs you almost nothing.
  • The balance is zeroed and outstanding is reduced before the ETH is sent, and the function carries nonReentrant. A recipient that calls back in finds the balance already zero and the guard already set.
  • Pausing the contract blocks new mints. It never blocks a withdrawal, and there is no setting that can.

Sweeping every node at once

withdrawAll walks the list of nodes the caller owns, zeroes each non-empty balance, and sends the sum to a single destination in one transfer. Nodes with nothing in them are skipped rather than reverting the sweep, so one empty node does not block the rest. The call reverts with NothingToWithdraw only when the whole sweep comes to zero.

SitowiseFactory.withdrawAll, the loop
for (uint256 i; i < ids.length; ++i) {
    uint256 id = ids[i];
    Node storage node = _node[id];
    uint256 bal = node.balance;
    if (bal == 0) continue;
    node.balance = 0;
    node.totalWithdrawn += uint128(bal);
    amount += bal;
    emit Withdrawn(id, to, bal);
}
if (amount == 0) revert NothingToWithdraw();

This loop is the reason the per-wallet cap exists at all. maxPerWallet starts at 25 and the owner cannot raise it above 100, because an unbounded cap could push the sweep past the block gas limit and leave a wallet unable to use it. Per-node withdraw keeps working regardless of how many nodes a wallet holds. See Limits.

The whole balance, always

There is no amount argument and no partial withdrawal. Both functions take everything the node has at that moment. If you want part of it somewhere else, withdraw to a wallet you control and split it from there.

Withdrawing does not close or change the node. Anything credited afterwards builds a new balance on the same id, and you withdraw that the same way. There is no reason to wait for a balance to grow except gas efficiency: nothing accrues faster for being left in place, and nothing is lost by taking it out.

What it costs

ItemCost
withdraw gasAbout 55,000, around $0.004, paid by you
withdrawAll gas, 25 nodesAbout 700,000, around $0.05, paid by you
Protocol fee on withdrawalNone. The full balance is transferred.
Minimum withdrawalNone, beyond needing a non-zero balance

Both gas figures were measured on chain at 0.0297 gwei with ETH around $2,450, and the table in the contracts README is where they come from. Gas prices move, so treat the gas numbers as the stable part and the dollar figures as what they were at that price.

Failure modes

ErrorCauseEffect on your balance
NotNodeOwnerThe sending address is not the node ownerNone. Send from the wallet that owns it.
NothingToWithdrawThe balance is zero, either because nothing has been credited since the last withdrawal or because withdrawAll already took itNone. There was nothing there to lose.
BadInputThe destination was the zero addressNone. Pass a real address.
TransferFailedThe destination rejected the ETH, for example a contract with no payable receiveNone. Withdraw to an address that can receive plain transfers.
ReentrancyThe destination called back into the factory while the transfer was still in flightNone. Withdraw to a plain wallet instead.
Every one of these reverts the whole transaction. A reverted withdrawal costs gas and changes nothing else: the balance, the node and the withdrawn figure are exactly as they were. Practical fixes for each are on Troubleshooting.

Withdrawing without the website

A withdrawal needs nothing from Sitowise’s servers. Everything it depends on is the contract and your key, so the Write Contract tab of the explorer at https://robinhoodchain.blockscout.com works, and so does cast. If this site were down or gone, the money would still come out.

cast
# one node
cast send $FACTORY "withdraw(uint256,address)" $NODE_ID $TO \
  --rpc-url https://rpc.mainnet.chain.robinhood.com --private-key $YOUR_KEY

# every node this wallet owns, in one transaction
cast send $FACTORY "withdrawAll(address)" $TO \
  --rpc-url https://rpc.mainnet.chain.robinhood.com --private-key $YOUR_KEY

The transaction must be sent from the address that owns the node. Where the ETH lands is a separate choice, described on Destination addresses. The deployed address is on Addresses.