Brain dump on "Thresher", inspired by playing with tornado.cash on ethereum (see http://gavinandresen.ninja/a-more-private-eth-wallet ). "Thresher" because it accumulates deposits until some minimum threshold is reached, then pays out.
The problem: tornado.cash deposits and withdrawals are fixed-size, with a minimum size (0.1 ETH in the case of ether). Anybody that uses tornado properly will accumulate less-than-minimum amounts of ETH in different addresses and be unable to spend them without compromising privacy.
Note: I'll be talking only about ETH for the rest of this gist, but the idea obviously applies to any of the tokens supported by tornado.cash.
Solution: a smart contract that accepts deposits that are less than 0.1 ETH with a tornado.cash 'note'. Once the contract has accumulated 0.1 ETH or more, it redeposits 0.1 ETH into tornado.cash with one of the deposit's notes, picked fairly at random (e.g. if you deposit 0.09 ETH your note has a 90% chance of being picked).
I'm going to make some people cringe and propose using a "good enough" way to pick the winners:
Winners are picked as a side effect of processing a new deposit at some current block height N.
The hash of block N-1 is used as the random seed to pick a winner. However, to make cheating by miners even more costly (they must pay transaction fees to another miner to get their entries on the list), only deposits received before block N-1 can win.
See "On Bitcoin as a public randomess source" by Bonneau, Clark, and Goldfeder for an analysis of miners trying to cheat by throwing away winning block hashes: https://pdfs.semanticscholar.org/ebae/9c7d91ea8b6a987642040a2142cc5ea67f7d.pdf Cheating only pays if miners can win more than twice what they earn mining a block; the reward is currently 2 ETH (plus fees), so we're OK using the block hash as our randomness source as long a cheating miner can't win more than 4 ETH.
Can depositors cheat? They cannot make it more likely that their entry is picked, because they cannot know what the block hash will be.
Tornado deposits are somewhat expensive-- on the order of a million gas. Each deposit to Thresher should include enough gas to cover a deposit, because each deposit could pick a winner and call tornado.deposit(). The contract could be written to pick multiple winners and make multiple tornado deposits at once (for once call to deposit()) if it accumulated 0.2 or more ether, but that would be bad because it would make the amount of gas required very unpredictable.
A Thresher user could try to cheat on gas costs by looking at the current state of the contract and seeing if their new deposit is likely to trigger a 'win' and a tornado deposit (e.g. most simply if their deposit will cause the Thresher balance to be >= 0.1 ETH). If it will (costing ~1million gas) they could wait and send their transaction later, after some other deposit has drained the contract.
I don't think I can save any storage. I need to store four pieces of information for every entry: current block height when the entry was received, address to receive winnings, how many ETH they contributed, and how much to pay out if they win. Currently 'how much to pay out' is their gasPrice*million+entryvalue, but I think it would be clearer to store it as how much to pay out. That won't save any storage, though.
I have to store both how much they contributed and how much to pay out to make the random draw fair.
Do you mean letting the depositor specify how much they'd like to get paid if they win? That's an interesting idea, I might do that. As long as it is less than 4 ETH it should work, and it would make it much more general.
currentBlock-1 is what I intended. If the contract is unused for several (or even thousands of) blocks then entries are kind of like schrodinger's cat-- they have neither won nor lost. Somebody with a pending entry could try to watch block hashes and submit another entry to trigger a "win" payout; but if more than one person tries to do that, the results will be random and fair over the long run.
That's why I don't want to use entry.blocknumber+1.