Sitowise
Contracts

Settlement

Money moves twice in Sitowise: once in, when a transfer buys a node, and once out, when a node’s owner takes their balance. This page is the mechanism for both, at the level of the calls the contract actually makes.

Sitowise, launch periodPaymentA plain transfer tothe payments walletWatcherSees the transfer andrecords the tx hashmintForThe relayer createsthe node, pays the gascreditBatchThe distributor sendsETH onto balancesNode balanceHeld on chain,counted in outstandingwithdrawYou call it yourself,the whole balance moves
Payment lands in a wallet, the relayer mints against that transaction, the distributor sends ETH onto balances, and the owner withdraws. Nothing in the chain of custody is a signature.

Why it looks like this

An earlier design kept balances off chain and settled them with EIP-712 vouchers: the server signed a cumulative allowance and you spent it against the contract. That saves gas when gas is expensive. Gas on this chain is around 0.03 gwei, so it saved nothing worth having and cost something that mattered, because it put a server key in the path between a holder and their own money. The vouchers are gone. Balances are held on chain and the owner moves them directly.

Nothing in the withdrawal path belongs to Sitowise
No signature, no session, no server, no expiry, no approval step, and no amount to request. If every Sitowise machine were switched off, every node owner could still take the whole balance they hold, from a block explorer, with the ABI alone.

A payment becomes a node

Payment happens entirely outside the contract. You send a plain 0.02 ETH transfer to the payments wallet. A watcher sees it and the relayer calls mintFor(to, paymentRef), paying that gas itself. The contract never holds purchase money, so there is no forwarding step that could fail and no treasury for it to sit in.

paymentRef is the payment transaction’s own hash. It is written into paymentRefUsed and emitted in NodeMinted, which is what makes the sale checkable rather than merely asserted: a node points at the exact transfer that paid for it, and that transfer can never point at a second node.

SitowiseFactory.mintFor
function mintFor(address to, bytes32 paymentRef) external onlyRelayer returns (uint256 id) {
    if (paused) revert IsPaused();
    if (to == address(0)) revert BadInput();
    if (paymentRef == bytes32(0)) revert BadInput();
    if (paymentRefUsed[paymentRef]) revert RefAlreadyUsed();
    if (_owned[to].length >= maxPerWallet) revert WalletLimit();

    paymentRefUsed[paymentRef] = true;

    id = ++totalNodes;
    // ...
    emit NodeMinted(id, to, paymentRef, uint64(block.timestamp));
}

Without that mapping the paymentRef in the log would prove nothing: one payment could back unlimited nodes and the explorer trail would be theatre. With it, the check anyone can run is simple. Take the node’s paymentRef, open that transaction, and confirm it paid the price to the payments wallet. Full steps are on Deploying a node.

ETH becomes a balance

Credits are decided off chain and settled on chain in one payable call. The distributor calls creditBatch(ids, amounts) and sends the money with it. If msg.value is not exactly the sum of amounts, the call reverts with ValueMismatch and nothing is recorded.

SitowiseFactory.creditBatch, the check that matters
uint256 sum;
for (uint256 i; i < n; ++i) {
    uint256 amt = amounts[i];
    if (amt == 0) revert BadInput();
    if (amt > type(uint128).max) revert AmountTooLarge();
    sum += amt;
}
if (sum != msg.value) revert ValueMismatch();

So a balance can never exist without the ETH behind it. There is no state in which the interface shows you a number that the contract cannot pay. The sum is validated before any storage is written, which also means a malformed batch costs the caller memory-only gas instead of a write per node.

The uint128 guard is not decoration either. Balances are uint128, and an explicit narrowing cast in Solidity truncates silently rather than reverting. Left unguarded, an oversized amount would credit a node less than the ETH backing it and break the accounting below permanently.

Each credited node emits Credited(id, amount, newBalance). The balance is withdrawable from that moment; there is no unlock, no vesting and no waiting period.

During the launch period rewards are funded by Sitowise, not by trading volume. The Uniswap v4 hook intended to replace that funding has no pools attached to it yet.

A balance becomes ETH in your wallet

The node’s owner calls withdraw(id, to), or withdrawAll(to) to sweep every node they hold in one transaction. The caller is checked against node.owner and against nothing else. It always moves the node’s whole balance, because a partial withdrawal would be an amount argument whose only purpose is to be got wrong.

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();
}

The balance is zeroed and the totals are updated before the ETH is sent, so a destination that calls back into the contract finds nothing left to take. The nonReentrant guard on top of that is belt and braces rather than the primary defence. Failure modes and gas are on Withdrawing.

Pausing blocks mintFor and nothing else. There is no state of the contract, and no action available to the owner, that stops or delays a withdrawal.

The invariant that holds it together

outstanding is the sum of every node balance. It rises by the exact msg.value of every credit and falls by the exact amount of every withdrawal. Its job is to bound what the contract owner can take out.

ReadMeans
outstanding()Everything the contract owes to node balances
freeBalance()address(this).balance - outstanding, contract funds attached to no node
isSolvent()balance >= outstanding. False would mean balances are not fully backed

rescue(to, amount) reverts with ExceedsFree for anything above freeBalance(). That is the whole guarantee: under every sequence of calls available to the owner, holder money is unreachable. It is enforced by a fuzz invariant in the test suite, and the invariant is verified by mutation, meaning the bound was deliberately changed to the full balance to confirm the suite fails when the property is broken. A test that cannot fail proves nothing. See Security model and Audits.

What used to be here

If you have read an older version of these docs, or a copy of them somewhere else, the following do not exist and never will in this deployment. None of them are functions on the contract at the published address.

Old mechanismWhat is true now
An EIP-712 voucher signed by a serverYou call the contract yourself. There is no signer key and no signature.
A cumulative allowance and a deadlineA balance, which does not expire and cannot be replayed because it is spent.
POST /api/withdraw/prepare and /confirmDeleted. Withdrawing touches no API at all.
A treasury contract receiving mint paymentsA plain payments wallet, outside the contract, holding no code.