Pseudonymous transfers of Ether tokens with low-traceability by using create2
& selfdestruct
Named after the spren from the Stormlight Archive series. Functions are intentionally (but poorly) lore-driven to encourage stronger understanding of smart contract behavior.
Alice wants to send Charlie some ether tokens but wants to conceal the transfer such that Charlie cannot trace the funds back to Alice’s EOA.
- Alice sends Ether to a designated EOA address, referred to as the escrow address (the address eventually gets bytecode via CREATE2)
- Alice (using a different wallet) registers Charlie as a recipient, and adds his address to the pool of other recipients
- Bob, permissionlessly, calls a function which deploys a self-destructible contract to one of the many escrow EOAs. The escrow contract is atomically SELFDESTRUCT'd and funds are sent to Charlie's address
- for pseudonymity to be successful, there needs to be many inbound/outbound parties such that salts and the CREATE2 addresses can be mixed
- shuffling the salts (off-chain) will obfuscate the origin of the funds
- for example: Alice intends to pay Charlie and Bob intends to pay Ed
- Alice's funds actually flow to Ed, and Bob's funds flow to Charlie
The largest privacy leak, at the moment, is publicly using salts to register recipients. These salts will reveal the original funder (and their intended recipient)
// obtain an address, that initially appears as an EOA
(address escrow, uint256 salt) = getAddress()
// alice sends money to the escrow EOA
(bool sent,) = address(escrow).call{value: 0.01 ether}("");
// alice registers Charlie as a recipient
// (alice preferably uses a different EOA)
addRecipient(address(charlie), salt)
// Bob, an external party, calls this function to route the transfer to charlie
function route(uint256 salt) external {
// deploys bytecode to the escrow EOA, converting it
// to a contract with self destruct functionality
address escrow = create2(..., salt);
// contract pseudo-randomly picks charlie as a recipient
// the pseudo-random selection is used to mask intention
address charlie = popRandomRecipient();
// selfdestruct the newly created contract, and direct the funds to charlie
Escrow(escrow).destroy(charlie);
}
There's some traceability in this single-party example, but with many inbound/outbound addresses you can achieve pseudonymity through obfuscation. The EOAs holding the funds (which get self-destructed) can be shuffled so Alice's funds never have a direct trace to Charlie's address.
Footguns galore! Whole lotta footguns here:
-
loss of funds -- if EOAs are under-funded or over-funded, the escrow will not deploy & self destruct. Funds will be frozen in the EOA
-
loss of funds -- if the salt returned by
spawn()
is lost, the funder cannot provide a recipient. Funds will be frozen in the EOA -
loss of privacy -- if no one calls
oathBreak()
with your salt, you may be forced to call it yourself, which will publicly signal your involvment with the Sylphrena contracts -
loss of decentralization -- to preserve privacy and maintain low-traceability, salts need to be managed off-chain. Managing salts on-chain could open the door for explicit or programmatic traceability
- off-chain management of salts inherently implies some governing entity. This can lead to both censorship and privacy leaks
- Tornado Cash
- Philogy's Blind Auctions with CREATE2
The code is not deployed anywhere, except for Ethereum Goerli.
The code is not battletested, and offers no guarantees of anonymity. Treat the code as a foundational starting point for a production-ready version, hence the MIT License.
I have no interest in supporting this code any further, as I am not looking to battle policymakers (and/or go to prison)
This example does not capture obfuscation since the funds directly flow from Alice's escrow to Charlie. With a sufficient pool of escrows & recipients, the entities can be shuffled to avoid having a direct flow of funds.