What is wrong with atomic swaps and how can channels help them, what important events occurred during the Constantinople hard fork, and what to do when you can't pay for gas.
The main motivation of any security specialist is the desire to avoid responsibility.
Providence was gracious, I left the ICO without waiting for the first irreversible transaction, but soon found myself developing a cryptocurrency exchange.
I am definitely not Malchish Kibalchish, and a single stern look is enough for me to hand over all keys and passwords. Therefore, my main goal as an architect was to position the blazing sting of cryptoanalysis as far away from the elements of infrastructure I cherish as possible.
Not your keys, not your problems.
We are building an asset exchange system and want to eliminate intermediate storage of these assets with us, but we must ensure the security of the transaction.
One can act as a judge in a disputed situation and conduct transactions with wallets requiring two out of three signatures: the buyer, the seller, and the escrow.
However, if a participant successfully attacks the escrow, they gain the desired two signatures.
An atomic swap is an exchange scheme where a smart contract serves as the guarantor, allowing only honest behavior.
Like in the riddle about the wolf, the goat, and the cabbage, you can only act according to a single correct scenario and incur losses if you deviate from it.
Only instead of greedy animals, the order is ensured by a hash function, so difficult to find a collision that it's hardly worth starting.
Step one: the riddle.
Suppose Alice wants to send Bob bitcoins for a handful of 'crypto yuen' one fine morning.
- She thinks of some big secret.
- She receives a hash from him.
- She transfers bitcoins to a smart contract, from which Bob can collect the money by presenting the secret (the hash of it must match what is specified in the contract).
- If Bob hasn't picked up his bitcoins by evening, Alice can take them back.
Step two: the bait.
Bob enters the game and transfers 'crypto euros' to his contract, which is written in such a way that:
- Alice can take the 'crypto yen' by presenting a secret number.
- Not earlier than noon, Bob can return the deposit if Alice does not show up.
Step three: guessing in the bait.
Alice comes for her money and takes the funds from Bob's contract, revealing her secret in the process.
Final step: the riddle is solved
Bob sees the transaction and with an eagle eye extracts the secret presented by Alice to the contract. He uses this secret to claim his bitcoins.
When something goes wrong
If Alice suddenly turns out to be mortal, Bob takes his yuan at lunch.
In turn, by evening, Alice returns the bitcoin if treacherous Bob decides to hold on to the money for better times.
If you prefer pictures to text, there's a more detailed and visual explanation for you on Habr. .
The difference in timeouts is designed to protect us from the malevolent Alice, who takes Bob's money at the very last moment, while he is nervously entering hex into the transaction and the timeout expires.
Participants cannot lose their money; at worst, they will have to wait for a refund.
Support in blockchainsThis is a simple, straightforward scheme that requires nothing from the interacting blockchains:
- Support for smart contracts with at least one branching
- Both blockchains must support the same hashing algorithms (don’t forget to check the length of the secret)
- Time locks.
At first glance, one could already say to the exchange 'goodbye, our meeting was a mistake,' but not so fast.
Despite all its advantages, atomic swap solutions do not impress with liquidity. Much of this is due to the fact that in the most popular pair BTC-USD, the fiat part was not fully tokenized.
The success of USDT has spawned a whole wave of stablecoins of the ERC20 format to suit any taste, from the custodial USDC to the algorithmic DAI.
Therefore, for simplicity, we will further discuss Alice selling bitcoins to Bob for some ERC20 tokens, and hope for the luck of the stabilizers, as we have many more technical problems ahead.
Speed
Bitcoin and Ethereum individually are not very fast, and here we have to wait first for one deposit with all confirmations, then for the second.
This is all because first, the participant who knows the secret makes a deposit, and the opponent waits for finality and only then transfers their part.
Moreover, we are dealing with a highly volatile asset, so during this time the rate can change significantly, and changing the terms is no easy task.
Confidentiality
Any exchange leaves artifacts on both blockchains. A keen observer may notice identical hashes in smart contracts and draw a logical conclusion that a transaction has occurred, leading to a plethora of deductions, from rates to taxes.
When the exchange knows your affairs, it’s quite unpleasant; when everyone knows, it’s doubly so.
Usability
The hallmark of blockchain in general and Ethereum in particular. Let’s review the actions that the seller and buyer need to undertake.
From the seller's perspective, it’s relatively simple: just transfer Bitcoin to a P2SH address. With Ethereum, things are much trickier.
ContractLet’s take a look at a typical GitHub contract for a swap:
contract iERC20 {
function totalSupply() public view returns (uint256);
function transfer(address receiver, uint numTokens) public returns (bool);
function balanceOf(address tokenOwner) public view returns (uint);
function approve(address delegate, uint numTokens) public returns (bool);
function allowance(address owner, address delegate) public view returns (uint);
function transferFrom(address owner, address buyer, uint numTokens) public returns (bool);
}
contract Swapper {
struct Swap {
iERC20 token;
bytes32 hash;
uint amount;
uint refundTime;
bytes32 secret;
}
mapping (address => mapping(address => Swap)) swaps;
function create(iERC20 token, bytes32 hash, address receiver, uint amount, uint refundTime) public {
require(swaps[msg.sender][receiver].amount == 0); // check if swap with given hash already exists
require(token.transferFrom(msg.sender, address(this), amount)); // transfer locked tokens to swap contract
swaps[msg.sender][receiver] = Swap(token, hash, amount, refundTime, 0x00); // create swap
}
function hashOf(bytes32 secret) public pure returns(bytes32) {
return sha256(abi.encodePacked(secret));
}
function withdraw(address owner, bytes32 secret) public {
Swap memory swap = swaps[owner][msg.sender];
require(swap.secret == bytes32(0));
require(swap.hash == sha256(abi.encodePacked(secret))); // swap exists
swaps[owner][msg.sender].secret = secret;
swap.token.transfer(msg.sender, swap.amount);
}
function refund(address receiver) public {
Swap memory swap = swaps[msg.sender][receiver];
require(now > swap.refundTime);
delete swaps[msg.sender][receiver];
swap.token.transfer(msg.sender, swap.amount);
}
}
Attention! Do not use this and other contracts from the article in production; they are written solely for demonstration purposes. Especially this one.
- Bob must invoke the method of the token contract
reject, granting the swap contract access to his tokens - Bob creates a swap and contract using the method
transferFromwhich takes the sender's tokens to his address - Alice in
withdrawreveals the secret and the contract callstransfer
Most wallets and crypto exchanges do not support reject tokens, and for good reason.
Users often make mistakes and simply transfer tokens to the contract, after which the tokens are simply lost. Comments on Etherscan are full of lamentations from the unfortunate.
To call the contract, you need to pay a fee in ETH, which means both participants must stock up on it before starting the deal, and very few want to deal with that.
Gasholder
First of all, we should remove sender verification wherever possible, and assume we have someone suffering from excess gas calling contracts for all willing parties.
Modernized contract
contract Swapper {
struct Swap {
iERC20 token;
address receiver;
uint amount;
address refundAddress;
uint refundTime;
}
mapping (bytes32 => Swap) swaps;
function create(iERC20 token, bytes32 hash, address receiver, uint amount, address refundAddress, uint refundTime) public {
require(swaps[hash].amount == 0); // use hash once
require(token.transferFrom(msg.sender, address(this), amount));
swaps[hash] = Swap(token, receiver, amount, refundAddress, refundTime);
}
function withdraw(bytes memory secret) public {
bytes32 hash = sha256(secret);
Swap memory swap = swaps[hash];
require(swap.amount > 0);
delete swaps[hash];
swap.token.transfer(swap.receiver, swap.amount);
}
function refund(bytes32 hash) public {
Swap memory swap = swaps[hash];
require(now > swap.refundTime);
delete swaps[hash];
swap.token.transfer(swap.refundAddress, swap.amount);
}
}
Contract-key dualism and EIP 712
As we know, an address on Ethereum can be a contract or an entity, in other words, a key.
The main task of the key is to sign various messages.
We can use Bob's contract as the sender, which performs all the necessary passes after verifying Bob's key signature.
Now, anyone can sponsor the participant's fee, but the decision is made only by the one who knows the key.
Bob's contract
library EIP712ProxyLibrary {
function hashCommand(address sender, iERC20 token, Swapper swapper, bytes32 hash, address receiver, uint amount, address refundAddress, uint refundTime) public view returns(bytes32);
}
contract ProxyBob {
address owner;
constructor(address _owner) public {
owner = _owner;
}
function createSwap(Swapper swapper, iERC20 token, bytes32 hash, address receiver, uint amount, address refundAddress, uint refundTime, uint8 v, bytes32 r, bytes32 s) public {
require(owner == ecrecover(EIP712ProxyLibrary.hashCommand(address(this), token, swapper, hash, receiver, amount, refundAddress, refundTime), v, r, s));
token.approve(address(swapper), amount);
swapper.create(token, hash, receiver, amount, refundAddress, refundTime);
}
}
To work with signatures of complex data structures in Ethereum, there is a standard , you can read more about it in
Divide and conquer
Often, the Ethereum contract hacking scenario looks like this:
- The participant deposits funds into the contract
- Then withdraws the funds
- Something goes wrong
- The attacker takes the money over and over again
If we return to our first example, something goes wrong if the mystery is an empty byte array.
How to steal a millionCreating a swap with a hash 0x66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925
This is the sha256 of 0x0000000000000000000000000000000000000000000000000000000000000000
We pass the secret and withdraw our tokens
We pass it again and withdraw someone else's, all because 0 = 0
By creating a separate contract for each transaction, we can isolate contracts at the EVM level.
But that's not all: now each transaction has its own address, to which tokens can be transferred from any wallet or exchange.
Abandoned contracts and create2
But now for each transaction, we have to create a contract and wait for the buyer to transfer their hard-earned 'crypto-funding' there. In the 'morning contracts, evening money' scheme, there is always the risk that the buyer will drop out, while the ether to create the contract is already spent.
Isn't there a way to have money in the morning and bytes in the evening?
In the Constantinople hard fork, the developers added the create2 instruction, which creates a new contract at a deterministic address
keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]
Where
- address — the factory contract address
- salt — some number, the meaning of which we will learn in the next installment
- init_code — the bytecode of the contract and the constructor parameters.
FactoryThe instruction only works via assembly, so the factory looks somewhat intimidating:
contract Factory {
event Deployed(address addr, uint256 salt);
function create2(bytes memory code, uint256 salt) public {
address addr;
assembly {
addr := create2(0, add(code, 0x20), mload(code), salt)
}
emit Deployed(addr, salt);
}
}
You can obtain your contract code using web3:
const MyContract = new web3.eth.Contract(ABI, {})
const code = MyContract.deploy({
data: BYTECODE,
arguments: constructorArgs
}).encodeABI();
const factory = new web3.eth.Contract(FACTORY_ABI, factoryAddress);
tx = factory.methods.create2(code, salt);
Due to limited support in Solidity, gas for the contract may be calculated incorrectly due to some nuances of ether.
Especially nice is that in case of insufficient gas, the contract fails with an internal error, not reporting that gas ran out, as one might expect.
Now we can transfer tokens to contracts without creating them in advance, and until we publish them on the network, no one will guess what the contract actually does.
A crow won't peck out another crow's eye.
It's clear that a true analyst, especially one who has received good investments for fighting the regime's adversaries through money laundering, won't be stopped by such childish tricks, and will still see the hash after the contract is created.
How can we ensure the hash doesn’t get exposed?
We transfer the swap off-chain: participants exchange signatures for transferring to the swap contract, and then the secret is privately revealed.
Step by step.Two multi-signatures are created, from which funds can be withdrawn with the signatures of Alice and Bob.
To ensure that anyone's offline departure doesn't become a tragedy, let's add a good old timeout.
Alice and Bob simultaneously make deposits.
- Alice generates a secret and sends Bob the hash of the secret and a transaction signature that transfers bitcoins to the swap address.
- Bob sends Alice a signature to withdraw tokens to the swap contract with the generated hash.
- Alice informs Bob of the secret.
At this moment, harmony is established: both Alice and Bob can finish the deal at any time. In this friendly atmosphere, they can exchange signatures to withdraw money to their final addresses.
To an outside observer, it appears as if the money passed through a 2 out of 2 multi-signature contract.
Additionally, this scheme allows both parties to make deposits simultaneously since the secret is generated after all confirmations.
Level 2.
Since we can withdraw money to one address and not publish the intermediate transaction, nothing prevents us from withdrawing money to multiple addresses and making an unlimited number of intermediate transactions. While it's not a necessary set for an exchange, once you've started gathering a swap, it's hard to stop.
Now Alice and Bob can fully engage. For example, they could automatically calculate the average price by exchanging at satoshis per second or simply connect the market maker to the liquidity recipient directly.
Step by step.
- The seller generates a secret and gives the buyer the hash of the secret and a transaction signature where part of the funds is transferred to the p2sh address of the swap, while the remainder returns to the seller's address.
- The buyer provides a signature that allows the tokens and change to be swapped to the recipient's address.
- The seller reveals a secret.
- History repeats itself with a new secret, adding a withdrawal of previously purchased tokens to the swap and change to the buyer's address, already paid to the seller's address.
We now have access to high-speed p2p trading; the key is to keep track of time and close the deal before the timeout.
However, by slightly adjusting our contracts, we can grant our channels immortality, greatly simplifying our network creation.
But we'll talk about that in the next episode.
Source: habr.com
