Escrow model
Each split creates a dedicatedSplitEscrow smart contract that holds funds until all participants have deposited. The contract is immutable once deployed: no one can change the participants, amounts, target, or fee after deployment.
Key properties:
- 2 to 50 participants: enforced in the constructor. A split needs at least 2 people and caps at 50 to prevent gas-based denial of service.
- Immutable parameters: amount per participant, target address, deadline, fee, and fee wallet are set at deploy time and cannot be modified.
- Separate storage: contributions and pending withdrawals are tracked separately from
address(this).balance, so force-sent ETH (via selfdestruct) cannot inflate fees or break invariants.
SIWE wallet binding
SplitBot uses Sign-In With Ethereum (SIWE) to bind Telegram accounts to Ethereum wallets. This prevents identity squatting, where one user registers another person’s address.1
Registration request
User runs
/register 0xWallet. The bot generates a cryptographic nonce and stores it in the database with a 10-minute TTL.2
Signature prompt
The bot returns a deep-link to the Mini App verification page, which displays a human-readable SIWE message containing the nonce, address, and Telegram user ID.
3
Wallet signature
The user signs the message in their wallet. No on-chain transaction is sent. The signature is returned to the bot via Telegram WebApp
sendData (mobile) or the /verify command (desktop fallback).4
Verification
The bot recovers the signer address from the signature using
eth_account.recover_message. If the recovered address matches the expected address, the binding is persisted.Fee-on-top
The contract charges a fee in basis points (BPS). The fee is deducted from the total pool before sending to the target, so participants pay slightly more than their share to ensure the target receives the exact amount. The math:feeAmount = pool * 50 / 10000 and sends pool - feeAmount to the target.
Deposit flow
Participants can deposit in two ways:deposit()function call: a standard EVM call through the Mini App’s wallet connect flow. This is the preferred path.- Direct ETH transfer: sending ETH to the contract address with sufficient value. The
receive()fallback accepts it.
_accept() function, which:
- Verifies the sender is a registered participant.
- Checks the deposit meets the minimum amount (amountPerParticipant).
- Rejects double deposits from the same address.
- Records the contribution and increments the paid count.
- Auto-executes the payout when
paidCount == participantCount.
Overpay is accepted. If a participant sends more than the required amount, the excess stays in the contract as dust. Anyone can call
sweepDust() after execution to send the dust to the target.Auto-execution
When the last participant deposits, the contract automatically executes:- Sets
executed = trueto prevent further deposits. - Computes
feeAmount = totalContributed * feeBps / 10000. - Sends
feeAmountto the fee wallet (with a 50,000 gas stipend). - Sends
totalContributed - feeAmountto the target (with a 50,000 gas stipend). - If either transfer fails, the amount goes into
pendingWithdrawalsfor the recipient to claim viaclaimRefund().
Refunds
If not all participants pay before the deadline, refunds are available through a pull-pattern:refund(): after the deadline, anyone can call this to queue all deposits intopendingWithdrawals. Each participant’s contribution is moved to their individual pending balance.claimRefund(): each participant withdraws their queued balance individually.pushPending(): anyone can claim on behalf of a stuck recipient (for example, a contract without aclaimRefundwrapper).
cancel(), which queues refunds for all participants.
Factory and CREATE2
TheSplitEscrowFactory deploys escrow contracts using CREATE2, which produces deterministic addresses based on:
- The factory’s address.
- A salt (derived from the split ID).
- The escrow bytecode and constructor arguments.
predictAddress() function.
The factory itself has no admin functions and no upgrade capability. A compromised deployer key can spam-deploy new escrows but cannot alter the factory’s behavior or affect existing splits.
Bot architecture
The Telegram bot runs as a long-lived Python process with three components:- Command handlers:
/register,/split,/status,/cancel,/history,/verify,/help. These handle user input and trigger contract interactions. - Background watcher: a repeating job that polls active splits every 30 seconds. It reads on-chain status and notifies the group when a split executes or gets refunded.
- WebApp data handler: receives signed SIWE proofs and payment confirmations from the Mini App via Telegram WebApp
sendData.