| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- // SPDX-License-Identifier: LGPLv3
- pragma solidity ^0.8.20;
- import {IERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
- import {SafeERC20} from "../lib/openzeppelin-contracts/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,
- address claimer,
- uint256 timeout1,
- uint256 timeout2,
- address asset,
- uint256 value,
- uint256 nonce
- );
- 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,
- _claimer,
- swap.timeout1,
- swap.timeout2,
- swap.asset,
- swap.value,
- swap.nonce
- );
- 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();
- }
- }
|