Files
atomic-swap/ethereum/contracts/SwapCreator.sol
2023-06-17 04:30:20 -05:00

334 lines
13 KiB
Solidity

// SPDX-License-Identifier: LGPLv3
pragma solidity ^0.8.19;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Secp256k1} from "./Secp256k1.sol";
// SwapCreator facilitates swapping between Alice, a party that has an EVM
// native currency or a token (ERC-20 or compatible API) that she wants to
// exchange cross-chain for a different currency, and Bob, a party that has the
// other chain's currency and wishes to exchange it for Alice's currency.
contract SwapCreator is Secp256k1 {
using SafeERC20 for IERC20;
// Stage represents the swap state. It is PENDING when `newSwap` is called
// to create and fund the swap. Alice sets Stage to READY, via `setReady`,
// after verifying that funds are locked on the other chain. Bob cannot
// claim the swap funds until Alice sets the swap Stage to READY. The Stage
// is set to COMPLETED when Bob claims directly via `claim` or indirectly
// via `claimRelayer`, or by Alice calling `refund`.
enum Stage {
INVALID,
PENDING,
READY,
COMPLETED
}
// swaps maps from a swap ID to the swap's current Stage
mapping(bytes32 => Stage) public swaps;
// Swap stores the swap parameters, the hash of which forms the swap ID.
struct Swap {
// owner is the address of Alice, who initiates the swap by calling
// `newSwap`. Only the owner is allowed to call `setReady` or `refund`.
address payable owner;
// claimer is the address of Bob. Only the claimer can call `claim` or
// sign a RelaySwap object that `claimRelayer` will accept the signature
// for.
address payable claimer;
// claimCommitment is the Keccak-256 hash of the expected secp256k1
// public key derived from the secret (private key) that Bob sends when
// claiming. Alice receives this commitment off-chain.
bytes32 claimCommitment;
// refundCommitment is the Keccak-256 hash of the expected secp256k1
// public key derived from the secret (private key) that Alice sends if
// refunding.
bytes32 refundCommitment;
// timeout1 is the block timestamp before which Alice can call
// either `setReady` or `refund`.
uint256 timeout1;
// timeout2 is the block timestamp after which Bob cannot claim, only
// Alice can refund.
uint256 timeout2;
// asset is address(0) for EVM native currency swaps, or it is the
// address of the token that Alice is providing.
address asset;
// value is the wei or token unit amount that Alice locked in the contract
uint256 value;
// nonce is a random value chosen by Alice
uint256 nonce;
}
// RelaySwap contains additional information required for relayed claim
// transactions. This entire structure is encoded and signed by the swap
// claimer, and the signature is passed to `claimRelayer`.
struct RelaySwap {
// swap specifies which swap is being claimed
Swap swap;
// fee is the wei amount paid to the relayer
uint256 fee;
// relayerHash Keccak-256 hash of (relayer's payout address || 4-byte salt)
bytes32 relayerHash;
// swapCreator is the address of the swap's contract
address swapCreator;
}
event New(
bytes32 swapID,
bytes32 claimKey,
bytes32 refundKey,
uint256 timeout1,
uint256 timeout2,
address asset,
uint256 value
);
event Ready(bytes32 indexed swapID);
event Claimed(bytes32 indexed swapID, bytes32 indexed s);
event Refunded(bytes32 indexed swapID, bytes32 indexed s);
// thrown when the value parameter to `newSwap` is zero
error ZeroValue();
// thrown when either of the claimCommitment or refundCommitment parameters
// passed to `newSwap` are zero
error InvalidSwapKey();
// thrown when the claimer parameter for `newSwap` is the zero address
error InvalidClaimer();
// thrown when the timeout1 or timeout2 parameters for `newSwap` are zero
error InvalidTimeout();
// thrown when msg.value of a `newSwap` transaction has the wrong value
error InvalidValue();
// thrown when trying to initiate a swap with an ID that already exists
error SwapAlreadyExists();
// thrown when trying to call `setReady` on a swap that is not in the
// PENDING stage
error SwapNotPending();
// thrown when the caller of `setReady` or `refund` is not the swap owner
error OnlySwapOwner();
// thrown when the signer of the relayed transaction is not the swap's
// claimer
error OnlySwapClaimer();
// thrown when trying to call `claim` or `refund` on an invalid swap
error InvalidSwap();
// thrown when trying to call `claim` or `refund` on a swap that's already
// completed
error SwapCompleted();
// thrown when trying to call `claim` on a swap that's not set to ready or
// the first timeout has not been reached
error TooEarlyToClaim();
// thrown when trying to call `claim` on a swap where the second timeout has
// been reached
error TooLateToClaim();
// thrown when it's the counterparty's turn to claim and refunding is not
// allowed
error NotTimeToRefund();
// thrown when the provided secret does not match its expected public key
// hash
error InvalidSecret();
// thrown when the signature of a `RelaySwap` is invalid
error InvalidSignature();
// thrown when the SwapCreator address is a `RelaySwap` is not the address
// of this contract
error InvalidContractAddress();
// thrown when the hash of the relayer address and salt passed to
// `claimRelayer` does not match the relayer hash in `RelaySwap`
error InvalidRelayerAddress();
// `newSwap` creates a new Swap instance using the passed parameters and
// locks Alice's native EVM currency or token asset in the contract. On
// success, the swap ID is returned.
//
// Note that the duration values are distinct from the timeout values:
//
// _timeoutDuration1:
// duration, in seconds, between the current block timestamp and
// timeout1
//
// _timeoutDuration2:
// duration, in seconds, between timeout1 and timeout2
//
function newSwap(
bytes32 _claimCommitment,
bytes32 _refundCommitment,
address payable _claimer,
uint256 _timeoutDuration1,
uint256 _timeoutDuration2,
address _asset,
uint256 _value,
uint256 _nonce
) public payable returns (bytes32) {
if (_value == 0) revert ZeroValue();
if (_asset == address(0)) {
if (_value != msg.value) revert InvalidValue();
} else {
// transfer the token amount to this contract
// WARN: fee-on-transfer tokens are not supported
IERC20(_asset).safeTransferFrom(msg.sender, address(this), _value);
}
if (_claimCommitment == 0 || _refundCommitment == 0) revert InvalidSwapKey();
if (_claimer == address(0)) revert InvalidClaimer();
if (_timeoutDuration1 == 0 || _timeoutDuration2 == 0) revert InvalidTimeout();
Swap memory swap = Swap({
owner: payable(msg.sender),
claimCommitment: _claimCommitment,
refundCommitment: _refundCommitment,
claimer: _claimer,
timeout1: block.timestamp + _timeoutDuration1,
timeout2: block.timestamp + _timeoutDuration1 + _timeoutDuration2,
asset: _asset,
value: _value,
nonce: _nonce
});
bytes32 swapID = keccak256(abi.encode(swap));
// ensure that we are not overriding an existing swap
if (swaps[swapID] != Stage.INVALID) revert SwapAlreadyExists();
emit New(
swapID,
_claimCommitment,
_refundCommitment,
swap.timeout1,
swap.timeout2,
swap.asset,
swap.value
);
swaps[swapID] = Stage.PENDING;
return swapID;
}
// Alice should call `setReady` before timeout1 and after verifying that Bob
// locked his swap funds.
function setReady(Swap memory _swap) public {
bytes32 swapID = keccak256(abi.encode(_swap));
if (swaps[swapID] != Stage.PENDING) revert SwapNotPending();
if (_swap.owner != msg.sender) revert OnlySwapOwner();
swaps[swapID] = Stage.READY;
emit Ready(swapID);
}
// Bob can call `claim` if either of these hold true:
// (1) Alice has set the swap to `ready` and it's before timeout1
// (2) It is between timeout1 and timeout2
function claim(Swap memory _swap, bytes32 _secret) public {
if (msg.sender != _swap.claimer) revert OnlySwapClaimer();
_claim(_swap, _secret);
if (_swap.asset == address(0)) {
// Transfer the swap value as the EVM's native currency
_swap.claimer.transfer(_swap.value);
} else {
// Transfer the swap value as a token amount.
// WARNING: this will FAIL for fee-on-transfer or rebasing tokens if
// the token transfer reverts (i.e. if this contract does not
// contain _swap.value tokens), exposing Bob's secret while giving
// him nothing.
IERC20(_swap.asset).safeTransfer(_swap.claimer, _swap.value);
}
}
// Anyone can call `claimRelayer` if they receive a signed _relaySwap object
// from Bob. The same rules for when Bob can call `claim` apply here when a
// 3rd party relays a claim for Bob. This version of claiming transfers a
// _relaySwap.fee to _relayer. To prevent front-running, while not requiring
// Bob to know the relayer's payout address, Bob only signs a salted hash of
// the relayer's payout address in _relaySwap.relayerHash.
// Note: claimRelayer will revert if the swap value is less than the relayer
// fee; in that case, Bob must call claim directly.
function claimRelayer(
RelaySwap memory _relaySwap,
bytes32 _secret,
address payable _relayer,
uint32 _salt,
uint8 v,
bytes32 r,
bytes32 s
) public {
address signer = ecrecover(keccak256(abi.encode(_relaySwap)), v, r, s);
if (signer != _relaySwap.swap.claimer) revert InvalidSignature();
if (address(this) != _relaySwap.swapCreator) revert InvalidContractAddress();
if (keccak256(abi.encodePacked(_relayer, _salt)) != _relaySwap.relayerHash)
revert InvalidRelayerAddress();
_claim(_relaySwap.swap, _secret);
// send ether to swap claimer, subtracting the relayer fee
if (_relaySwap.swap.asset == address(0)) {
_relaySwap.swap.claimer.transfer(_relaySwap.swap.value - _relaySwap.fee);
payable(_relayer).transfer(_relaySwap.fee);
} else {
// WARN: this will FAIL for fee-on-transfer or rebasing tokens if the token
// transfer reverts (i.e. if this contract does not contain _swap.value tokens),
// exposing Bob's secret while giving him nothing.
IERC20(_relaySwap.swap.asset).safeTransfer(
_relaySwap.swap.claimer,
_relaySwap.swap.value - _relaySwap.fee
);
IERC20(_relaySwap.swap.asset).safeTransfer(_relayer, _relaySwap.fee);
}
}
function _claim(Swap memory _swap, bytes32 _secret) internal {
bytes32 swapID = keccak256(abi.encode(_swap));
Stage swapStage = swaps[swapID];
if (swapStage == Stage.INVALID) revert InvalidSwap();
if (swapStage == Stage.COMPLETED) revert SwapCompleted();
if (block.timestamp < _swap.timeout1 && swapStage != Stage.READY) revert TooEarlyToClaim();
if (block.timestamp >= _swap.timeout2) revert TooLateToClaim();
verifySecret(_secret, _swap.claimCommitment);
emit Claimed(swapID, _secret);
swaps[swapID] = Stage.COMPLETED;
}
// Alice can `refund` her swap funds:
// - Until timeout1, unless she called `setReady`
// - After timeout2, independent of whether she called `setReady`
function refund(Swap memory _swap, bytes32 _secret) public {
bytes32 swapID = keccak256(abi.encode(_swap));
Stage swapStage = swaps[swapID];
if (swapStage == Stage.INVALID) revert InvalidSwap();
if (swapStage == Stage.COMPLETED) revert SwapCompleted();
if (_swap.owner != msg.sender) revert OnlySwapOwner();
if (
block.timestamp < _swap.timeout2 &&
(block.timestamp > _swap.timeout1 || swapStage == Stage.READY)
) revert NotTimeToRefund();
verifySecret(_secret, _swap.refundCommitment);
emit Refunded(swapID, _secret);
// send asset back to swap owner
swaps[swapID] = Stage.COMPLETED;
if (_swap.asset == address(0)) {
_swap.owner.transfer(_swap.value);
} else {
IERC20(_swap.asset).safeTransfer(_swap.owner, _swap.value);
}
}
function verifySecret(bytes32 _secret, bytes32 _hashedPubkey) internal pure {
if (!mulVerify(uint256(_secret), uint256(_hashedPubkey))) revert InvalidSecret();
}
}