USDT compatibility (#484)

This commit is contained in:
Dmitry Holodov
2023-06-17 04:30:20 -05:00
committed by GitHub
parent 9e3ef7b527
commit 8941163c00
38 changed files with 1161 additions and 427 deletions

View File

@@ -14,7 +14,7 @@ lint-shell:
.PHONY: lint-solidity
lint-solidity:
"$$(npm config get prefix)/bin/solhint" $$(find ethereum -name '*.sol')
"$$(npm config get prefix)/bin/solhint" $$(find ethereum -name '*.sol' -not -path '**/@openzeppelin/**')
.PHONY: lint
lint: lint-go lint-shell lint-solidity
@@ -30,7 +30,8 @@ format-shell:
.PHONY: format-solidity
format-solidity:
"$$(npm config get prefix)/bin/prettier" --print-width 100 --write $$(find ethereum -name '*.sol')
"$$(npm config get prefix)/bin/prettier" --print-width 100 --write \
$$(find ethereum -name '*.sol' -not -path '**/@openzeppelin/**')
.PHONY: format
format: format-go format-shell format-solidity

View File

@@ -1139,8 +1139,8 @@ func runGetContractSwapInfo(ctx *cli.Context) error {
fmt.Printf("Swap struct as stored in the contract:\n")
fmt.Printf("\tOwner: %s\n", resp.Swap.Owner)
fmt.Printf("\tClaimer: %s\n", resp.Swap.Claimer)
fmt.Printf("\tPubKeyClaim: %x\n", resp.Swap.PubKeyClaim)
fmt.Printf("\tPubKeyRefund: %x\n", resp.Swap.PubKeyRefund)
fmt.Printf("\tClaimCommitment: %x\n", resp.Swap.ClaimCommitment)
fmt.Printf("\tRefundCommitment: %x\n", resp.Swap.RefundCommitment)
fmt.Printf("\tTimeout1: %s\n", resp.Swap.Timeout1)
fmt.Printf("\tTimeout2: %s\n", resp.Swap.Timeout2)
fmt.Printf("\tAsset: %s\n", resp.Swap.Asset)

View File

@@ -66,7 +66,7 @@ func MainnetConfig() *Config {
Port: DefaultMoneroDaemonMainnetPort,
},
},
SwapCreatorAddr: ethcommon.HexToAddress("0xa55aa5557ec22e85804729bc6935029bb84cf16a"),
SwapCreatorAddr: ethcommon.HexToAddress("0x377ed3a60007048DF00135637521170628De89E5"),
Bootnodes: []string{
"/ip4/67.205.131.11/tcp/9909/p2p/12D3KooWGpCLC4y42rf6aR3cguVFJAruzFXT6mUEyp7C32jTsyJd",
"/ip4/143.198.123.27/tcp/9909/p2p/12D3KooWDCE2ukB1Sw88hmLFk5BZRRViyYLeuAKPuu59nYyFWAec",
@@ -99,7 +99,7 @@ func StagenetConfig() *Config {
Port: 38081,
},
},
SwapCreatorAddr: ethcommon.HexToAddress("0xbf2B7a6dCE5598Cf002B3507a8D62cf2C35cE5c6"),
SwapCreatorAddr: ethcommon.HexToAddress("0x377ed3a60007048DF00135637521170628De89E5"),
Bootnodes: []string{
"/ip4/134.122.115.208/tcp/9900/p2p/12D3KooWHZ2G9XscjDGvG7p8uPBoYerDc9kWYnc8oJFGfFxS6gfq",
"/ip4/143.198.123.27/tcp/9900/p2p/12D3KooWGzExs5zqebnDvqkUAKaiUuxF3DNbrfJ4prbfkxjXb366",

View File

@@ -1,18 +1,21 @@
package daemon
import (
"context"
"sync"
"testing"
"time"
"github.com/cockroachdb/apd/v3"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/athanorlabs/atomic-swap/coins"
"github.com/athanorlabs/atomic-swap/common/rpctypes"
"github.com/athanorlabs/atomic-swap/common/types"
contracts "github.com/athanorlabs/atomic-swap/ethereum"
"github.com/athanorlabs/atomic-swap/ethereum/extethclient"
"github.com/athanorlabs/atomic-swap/monero"
"github.com/athanorlabs/atomic-swap/rpcclient"
"github.com/athanorlabs/atomic-swap/tests"
@@ -21,26 +24,52 @@ import (
// Tests the scenario where Bob has XMR and enough ETH to pay gas fees for the token claim. He
// exchanges 2 XMR for 3 of Alice's ERC20 tokens.
func TestRunSwapDaemon_ExchangesXMRForERC20Tokens(t *testing.T) {
minXMR := coins.StrToDecimal("1")
maxXMR := coins.StrToDecimal("2")
exRate := coins.StrToExchangeRate("1.5")
fundingEC := extethclient.CreateTestClient(t, tests.GetTakerTestKey(t))
tokenAsset := getMockTetherAsset(t, fundingEC)
tokenAddr := tokenAsset.Address()
token, err := fundingEC.ERC20Info(context.Background(), tokenAddr)
require.NoError(t, err)
bobConf := CreateTestConf(t, tests.GetMakerTestKey(t))
minXMR := coins.StrToDecimal("0.1")
maxXMR := coins.StrToDecimal("0.25")
exRate := coins.StrToExchangeRate("140")
providesAmt := coins.NewEthAssetAmount(coins.StrToDecimal("33.999994"), token) // 33.999994 USDT / 140 = 0.2428571 XMR
gasMoney := coins.EtherToWei(coins.StrToDecimal("0.1"))
bobEthKey, err := crypto.GenerateKey()
require.NoError(t, err)
bobConf := CreateTestConf(t, bobEthKey)
monero.MineMinXMRBalance(t, bobConf.MoneroClient, coins.MoneroToPiconero(maxXMR))
aliceConf := CreateTestConf(t, tests.GetTakerTestKey(t))
tokenAsset := getMockTetherAsset(t, aliceConf.EthereumClient)
// Ensure that Alice has no tokens and definitely no pre-approval to spend
// any of those tokens by giving her a brand-new ETH key.
aliceEthKey, err := crypto.GenerateKey()
require.NoError(t, err)
aliceConf := CreateTestConf(t, aliceEthKey)
timeout := 7 * time.Minute
// Fund Alice and Bob with a little ether for gas. Bob needs gas to claim,
// as ERC20 token swaps cannot use a relayer.
_, err = fundingEC.Transfer(context.Background(), aliceConf.EthereumClient.Address(), gasMoney, nil)
require.NoError(t, err)
_, err = fundingEC.Transfer(context.Background(), bobConf.EthereumClient.Address(), gasMoney, nil)
require.NoError(t, err)
// Fund Alice with the exact amount of token that she'll provide in the swap
// with Bob. After the swap is over, her token balance should be exactly
// zero.
erc20Iface, err := contracts.NewIERC20(tokenAddr, fundingEC.Raw())
require.NoError(t, err)
txOpts, err := fundingEC.TxOpts(context.Background())
require.NoError(t, err)
_, err = erc20Iface.Transfer(txOpts, aliceConf.EthereumClient.Address(), providesAmt.BigInt())
require.NoError(t, err)
ctx, _ := LaunchDaemons(t, timeout, aliceConf, bobConf)
bc := rpcclient.NewClient(ctx, bobConf.RPCPort)
ac := rpcclient.NewClient(ctx, aliceConf.RPCPort)
bobStartTokenBal, err := bobConf.EthereumClient.ERC20Balance(ctx, tokenAsset.Address())
require.NoError(t, err)
_, bobStatusCh, err := bc.MakeOfferAndSubscribe(minXMR, maxXMR, exRate, tokenAsset, false)
require.NoError(t, err)
time.Sleep(250 * time.Millisecond) // offer propagation time
@@ -52,12 +81,9 @@ func TestRunSwapDaemon_ExchangesXMRForERC20Tokens(t *testing.T) {
require.Len(t, peersWithOffers[0].Offers, 1)
peerID := peersWithOffers[0].PeerID
offer := peersWithOffers[0].Offers[0]
tokenInfo, err := ac.TokenInfo(offer.EthAsset.Address())
require.NoError(t, err)
providesAmt, err := exRate.ToERC20Amount(offer.MaxAmount, tokenInfo)
require.NoError(t, err)
require.Equal(t, tokenAddr.String(), offer.EthAsset.Address().String())
aliceStatusCh, err := ac.TakeOfferAndSubscribe(peerID, offer.ID, providesAmt)
aliceStatusCh, err := ac.TakeOfferAndSubscribe(peerID, offer.ID, providesAmt.AsStd())
require.NoError(t, err)
var statusWG sync.WaitGroup
@@ -105,14 +131,19 @@ func TestRunSwapDaemon_ExchangesXMRForERC20Tokens(t *testing.T) {
}
//
// Check Bob's token balance via RPC method instead of doing it directly
// Check final token balances via RPC method instead of doing it directly on
// the eth client. Bob should have exactly the provided amount and Alice's
// token balance should now be zero.
//
endBalances, err := bc.Balances(&rpctypes.BalancesRequest{TokenAddrs: []ethcommon.Address{tokenAsset.Address()}})
require.NoError(t, err)
require.NotEmpty(t, endBalances.TokenBalances)
balReq := &rpctypes.BalancesRequest{TokenAddrs: []ethcommon.Address{tokenAddr}}
delta := new(apd.Decimal)
_, err = coins.DecimalCtx().Sub(delta, endBalances.TokenBalances[0].Amount, bobStartTokenBal.Amount)
bobBal, err := bc.Balances(balReq)
require.NoError(t, err)
require.Equal(t, providesAmt.Text('f'), delta.Text('f'))
require.NotEmpty(t, bobBal.TokenBalances)
require.Equal(t, providesAmt.AsStdString(), bobBal.TokenBalances[0].AsStdString())
aliceBal, err := ac.Balances(balReq)
require.NoError(t, err)
require.NotEmpty(t, aliceBal.TokenBalances)
require.Equal(t, "0", aliceBal.TokenBalances[0].AsStdString())
}

View File

@@ -38,15 +38,15 @@ func TestRecoveryDB_ContractSwapInfo(t *testing.T) {
StartNumber: big.NewInt(12345),
SwapID: types.Hash{1, 2, 3, 4},
Swap: &contracts.SwapCreatorSwap{
Owner: ethcommon.HexToAddress("0xda9dfa130df4de4673b89022ee50ff26f6ea73cf"),
Claimer: ethcommon.HexToAddress("0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"),
PubKeyClaim: ethcommon.HexToHash("0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9"),
PubKeyRefund: ethcommon.HexToHash("0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14"),
Timeout1: big.NewInt(1672531200),
Timeout2: big.NewInt(1672545600),
Asset: types.EthAssetETH.Address(),
Value: big.NewInt(9876),
Nonce: big.NewInt(1234),
Owner: ethcommon.HexToAddress("0xda9dfa130df4de4673b89022ee50ff26f6ea73cf"),
Claimer: ethcommon.HexToAddress("0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"),
ClaimCommitment: ethcommon.HexToHash("0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9"),
RefundCommitment: ethcommon.HexToHash("0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14"),
Timeout1: big.NewInt(1672531200),
Timeout2: big.NewInt(1672545600),
Asset: types.EthAssetETH.Address(),
Value: big.NewInt(9876),
Nonce: big.NewInt(1234),
},
SwapCreatorAddr: ethcommon.HexToAddress("0xd2b5d6252d0645e4cf4bb547e82a485f527befb7"),
}
@@ -57,8 +57,8 @@ func TestRecoveryDB_ContractSwapInfo(t *testing.T) {
"swap": {
"owner": "0xda9dfa130df4de4673b89022ee50ff26f6ea73cf",
"claimer": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8",
"pubKeyClaim": "0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9",
"pubKeyRefund": "0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14",
"claimCommitment": "0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9",
"refundCommitment": "0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14",
"timeout1": 1672531200,
"timeout2": 1672545600,
"asset": "0x0000000000000000000000000000000000000000",
@@ -150,15 +150,15 @@ func TestRecoveryDB_DeleteSwap(t *testing.T) {
StartNumber: big.NewInt(12345),
SwapID: types.Hash{1, 2, 3, 4},
Swap: &contracts.SwapCreatorSwap{
Owner: ethcommon.HexToAddress("0xda9dfa130df4de4673b89022ee50ff26f6ea73cf"),
Claimer: ethcommon.HexToAddress("0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"),
PubKeyClaim: ethcommon.HexToHash("0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9"),
PubKeyRefund: ethcommon.HexToHash("0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14"),
Timeout1: big.NewInt(1672531200),
Timeout2: big.NewInt(1672545600),
Asset: types.EthAssetETH.Address(),
Value: big.NewInt(9876),
Nonce: big.NewInt(1234),
Owner: ethcommon.HexToAddress("0xda9dfa130df4de4673b89022ee50ff26f6ea73cf"),
Claimer: ethcommon.HexToAddress("0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"),
ClaimCommitment: ethcommon.HexToHash("0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9"),
RefundCommitment: ethcommon.HexToHash("0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14"),
Timeout1: big.NewInt(1672531200),
Timeout2: big.NewInt(1672545600),
Asset: types.EthAssetETH.Address(),
Value: big.NewInt(9876),
Nonce: big.NewInt(1234),
},
SwapCreatorAddr: ethcommon.HexToAddress("0xd2b5d6252d0645e4cf4bb547e82a485f527befb7"),
}

View File

@@ -32,7 +32,7 @@ var (
// UTContractMetaData contains all meta data concerning the UTContract contract.
var UTContractMetaData = &bind.MetaData{
ABI: "[{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_stamp\",\"type\":\"uint256\"}],\"name\":\"checkStamp\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]",
Bin: "0x608060405234801561001057600080fd5b506101da806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063d5f7a03a14610030575b600080fd5b61004a600480360381019061004591906100d4565b61004c565b005b8042111561008f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161008690610184565b60405180910390fd5b8060008190555050565b600080fd5b6000819050919050565b6100b18161009e565b81146100bc57600080fd5b50565b6000813590506100ce816100a8565b92915050565b6000602082840312156100ea576100e9610099565b5b60006100f8848285016100bf565b91505092915050565b600082825260208201905092915050565b7f626c6f636b2e74696d657374616d7020776173206e6f74206c6573732074686160008201527f6e207374616d7000000000000000000000000000000000000000000000000000602082015250565b600061016e602783610101565b915061017982610112565b604082019050919050565b6000602082019050818103600083015261019d81610161565b905091905056fea2646970667358221220e3debdc387582462e246390796d566e062d5965fb0bd8de5af573ad815d24b0564736f6c63430008130033",
Bin: "0x608060405234801561000f575f80fd5b506101cb8061001d5f395ff3fe608060405234801561000f575f80fd5b5060043610610029575f3560e01c8063d5f7a03a1461002d575b5f80fd5b610047600480360381019061004291906100cc565b610049565b005b8042111561008c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161008390610177565b60405180910390fd5b805f8190555050565b5f80fd5b5f819050919050565b6100ab81610099565b81146100b5575f80fd5b50565b5f813590506100c6816100a2565b92915050565b5f602082840312156100e1576100e0610095565b5b5f6100ee848285016100b8565b91505092915050565b5f82825260208201905092915050565b7f626c6f636b2e74696d657374616d7020776173206e6f74206c657373207468615f8201527f6e207374616d7000000000000000000000000000000000000000000000000000602082015250565b5f6101616027836100f7565b915061016c82610107565b604082019050919050565b5f6020820190508181035f83015261018e81610155565b905091905056fea264697066735822122077057003501b23f47c692e417ddc5f1429b9309ebe23d189b6906de5aa0d260964736f6c63430008140033",
}
// UTContractABI is the input ABI used to generate the binding from.

File diff suppressed because one or more lines are too long

View File

@@ -5,17 +5,17 @@ package contracts
// gas limit. We use these values to estimate minimum required balances.
const (
MaxNewSwapETHGas = 50639
MaxNewSwapTokenGas = 86218
MaxNewSwapTokenGas = 87369
MaxSetReadyGas = 32054
MaxClaimETHGas = 43349
MaxClaimTokenGas = 47522
MaxClaimTokenGas = 48416
MaxRefundETHGas = 43132
MaxRefundTokenGas = 47294
MaxRefundTokenGas = 48327
MaxTokenApproveGas = 47000 // 46223 with our contract
)
// constants that are interesting to track, but not used by swaps
const (
maxSwapCreatorDeployGas = 1094089
maxTestERC20DeployGas = 905727 // using long token names or symbols will increase this
maxSwapCreatorDeployGas = 1179944
maxTestERC20DeployGas = 932965 // using long token names or symbols will increase this
)

View File

@@ -1,11 +1,11 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC20/ERC20.sol)
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/ERC20.sol)
pragma solidity ^0.8.0;
import {IERC20} from "./IERC20.sol";
import {IERC20Metadata} from "./IERC20Metadata.sol";
import {Context} from "./Context.sol";
import "./IERC20.sol";
import "./extensions/IERC20Metadata.sol";
import "../../utils/Context.sol";
/**
* @dev Implementation of the {IERC20} interface.
@@ -18,6 +18,9 @@ import {Context} from "./Context.sol";
* https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How
* to implement supply mechanisms].
*
* The default value of {decimals} is 18. To change this, you should override
* this function so it returns a different value.
*
* We have followed general OpenZeppelin Contracts guidelines: functions revert
* instead returning `false` on failure. This behavior is nonetheless
* conventional and does not conflict with the expectations of ERC20
@@ -45,9 +48,6 @@ contract ERC20 is Context, IERC20, IERC20Metadata {
/**
* @dev Sets the values for {name} and {symbol}.
*
* The default value of {decimals} is 18. To select a different value for
* {decimals} you should overload it.
*
* All two of these values are immutable: they can only be set once during
* construction.
*/
@@ -77,8 +77,8 @@ contract ERC20 is Context, IERC20, IERC20Metadata {
* be displayed to a user as `5.05` (`505 / 10 ** 2`).
*
* Tokens usually opt for a value of 18, imitating the relationship between
* Ether and Wei. This is the value {ERC20} uses, unless this function is
* overridden;
* Ether and Wei. This is the default value returned by this function, unless
* it's overridden.
*
* NOTE: This information is only used for _display_ purposes: it in
* no way affects any of the arithmetic of the contract, including
@@ -119,10 +119,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata {
/**
* @dev See {IERC20-allowance}.
*/
function allowance(
address owner,
address spender
) public view virtual override returns (uint256) {
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}
@@ -158,11 +155,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata {
* - the caller must have allowance for ``from``'s tokens of at least
* `amount`.
*/
function transferFrom(
address from,
address to,
uint256 amount
) public virtual override returns (bool) {
function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
@@ -201,10 +194,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata {
* - `spender` must have allowance for the caller of at least
* `subtractedValue`.
*/
function decreaseAllowance(
address spender,
uint256 subtractedValue
) public virtual returns (bool) {
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
address owner = _msgSender();
uint256 currentAllowance = allowance(owner, spender);
require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
@@ -355,7 +345,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata {
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {} // solhint-disable-line
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {}
/**
* @dev Hook that is called after any transfer of tokens. This includes
@@ -371,5 +361,5 @@ contract ERC20 is Context, IERC20, IERC20Metadata {
*
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {} // solhint-disable-line
function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {}
}

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol)
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.0;

View File

@@ -3,7 +3,7 @@
pragma solidity ^0.8.0;
import {IERC20} from "./IERC20.sol";
import "../IERC20.sol";
/**
* @dev Interface for the optional metadata functions from the ERC20 standard.

View File

@@ -0,0 +1,60 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/IERC20Permit.sol)
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in
* https://eips.ethereum.org/EIPS/eip-2612[EIP-2612].
*
* Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by
* presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't
* need to send a transaction, and thus is not required to hold Ether at all.
*/
interface IERC20Permit {
/**
* @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens,
* given ``owner``'s signed approval.
*
* IMPORTANT: The same issues {IERC20-approve} has related to transaction
* ordering also apply here.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `spender` cannot be the zero address.
* - `deadline` must be a timestamp in the future.
* - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner`
* over the EIP712-formatted function arguments.
* - the signature must use ``owner``'s current nonce (see {nonces}).
*
* For more information on the signature format, see the
* https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP
* section].
*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external;
/**
* @dev Returns the current nonce for `owner`. This value must be
* included whenever a signature is generated for {permit}.
*
* Every successful call to {permit} increases ``owner``'s nonce by one. This
* prevents a signature from being used multiple times.
*/
function nonces(address owner) external view returns (uint256);
/**
* @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}.
*/
// solhint-disable-next-line func-name-mixedcase
function DOMAIN_SEPARATOR() external view returns (bytes32);
}

View File

@@ -0,0 +1,143 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/utils/SafeERC20.sol)
pragma solidity ^0.8.0;
import "../IERC20.sol";
import "../extensions/IERC20Permit.sol";
import "../../../utils/Address.sol";
/**
* @title SafeERC20
* @dev Wrappers around ERC20 operations that throw on failure (when the token
* contract returns false). Tokens that return no value (and instead revert or
* throw on failure) are also supported, non-reverting calls are assumed to be
* successful.
* To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
*/
library SafeERC20 {
using Address for address;
/**
* @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeTransfer(IERC20 token, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
}
/**
* @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
* calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
*/
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
}
/**
* @dev Deprecated. This function has issues similar to the ones found in
* {IERC20-approve}, and its usage is discouraged.
*
* Whenever possible, use {safeIncreaseAllowance} and
* {safeDecreaseAllowance} instead.
*/
function safeApprove(IERC20 token, address spender, uint256 value) internal {
// safeApprove should only be called when setting an initial allowance,
// or when resetting it to zero. To increase and decrease it, use
// 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
require(
(value == 0) || (token.allowance(address(this), spender) == 0),
"SafeERC20: approve from non-zero to non-zero allowance"
);
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
}
/**
* @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
uint256 oldAllowance = token.allowance(address(this), spender);
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance + value));
}
/**
* @dev Decrease the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal {
unchecked {
uint256 oldAllowance = token.allowance(address(this), spender);
require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance - value));
}
}
/**
* @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful. Compatible with tokens that require the approval to be set to
* 0 before setting it to a non-zero value.
*/
function forceApprove(IERC20 token, address spender, uint256 value) internal {
bytes memory approvalCall = abi.encodeWithSelector(token.approve.selector, spender, value);
if (!_callOptionalReturnBool(token, approvalCall)) {
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, 0));
_callOptionalReturn(token, approvalCall);
}
}
/**
* @dev Use a ERC-2612 signature to set the `owner` approval toward `spender` on `token`.
* Revert on invalid signature.
*/
function safePermit(
IERC20Permit token,
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) internal {
uint256 nonceBefore = token.nonces(owner);
token.permit(owner, spender, value, deadline, v, r, s);
uint256 nonceAfter = token.nonces(owner);
require(nonceAfter == nonceBefore + 1, "SafeERC20: permit did not succeed");
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*/
function _callOptionalReturn(IERC20 token, bytes memory data) private {
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
// we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that
// the target address contains contract code and also asserts for success in the low-level call.
bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
require(returndata.length == 0 || abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead.
*/
function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
// we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false
// and not revert is the subcall reverts.
(bool success, bytes memory returndata) = address(token).call(data);
return
success && (returndata.length == 0 || abi.decode(returndata, (bool))) && Address.isContract(address(token));
}
}

View File

@@ -0,0 +1,244 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (utils/Address.sol)
pragma solidity ^0.8.1;
/**
* @dev Collection of functions related to the address type
*/
library Address {
/**
* @dev Returns true if `account` is a contract.
*
* [IMPORTANT]
* ====
* It is unsafe to assume that an address for which this function returns
* false is an externally-owned account (EOA) and not a contract.
*
* Among others, `isContract` will return false for the following
* types of addresses:
*
* - an externally-owned account
* - a contract in construction
* - an address where a contract will be created
* - an address where a contract lived, but was destroyed
*
* Furthermore, `isContract` will also return true if the target contract within
* the same transaction is already scheduled for destruction by `SELFDESTRUCT`,
* which only has an effect at the end of a transaction.
* ====
*
* [IMPORTANT]
* ====
* You shouldn't rely on `isContract` to protect against flash loan attacks!
*
* Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets
* like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract
* constructor.
* ====
*/
function isContract(address account) internal view returns (bool) {
// This method relies on extcodesize/address.code.length, which returns 0
// for contracts in construction, since the code is only stored at the end
// of the constructor execution.
return account.code.length > 0;
}
/**
* @dev Replacement for Solidity's `transfer`: sends `amount` wei to
* `recipient`, forwarding all available gas and reverting on errors.
*
* https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
* of certain opcodes, possibly making contracts go over the 2300 gas limit
* imposed by `transfer`, making them unable to receive funds via
* `transfer`. {sendValue} removes this limitation.
*
* https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more].
*
* IMPORTANT: because control is transferred to `recipient`, care must be
* taken to not create reentrancy vulnerabilities. Consider using
* {ReentrancyGuard} or the
* https://solidity.readthedocs.io/en/v0.8.0/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
*/
function sendValue(address payable recipient, uint256 amount) internal {
require(address(this).balance >= amount, "Address: insufficient balance");
(bool success, ) = recipient.call{value: amount}("");
require(success, "Address: unable to send value, recipient may have reverted");
}
/**
* @dev Performs a Solidity function call using a low level `call`. A
* plain `call` is an unsafe replacement for a function call: use this
* function instead.
*
* If `target` reverts with a revert reason, it is bubbled up by this
* function (like regular Solidity function calls).
*
* Returns the raw returned data. To convert to the expected return value,
* use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
*
* Requirements:
*
* - `target` must be a contract.
* - calling `target` with `data` must not revert.
*
* _Available since v3.1._
*/
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, "Address: low-level call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
* `errorMessage` as a fallback revert reason when `target` reverts.
*
* _Available since v3.1._
*/
function functionCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but also transferring `value` wei to `target`.
*
* Requirements:
*
* - the calling contract must have an ETH balance of at least `value`.
* - the called Solidity function must be `payable`.
*
* _Available since v3.1._
*/
function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
}
/**
* @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
* with `errorMessage` as a fallback revert reason when `target` reverts.
*
* _Available since v3.1._
*/
function functionCallWithValue(
address target,
bytes memory data,
uint256 value,
string memory errorMessage
) internal returns (bytes memory) {
require(address(this).balance >= value, "Address: insufficient balance for call");
(bool success, bytes memory returndata) = target.call{value: value}(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
return functionStaticCall(target, data, "Address: low-level static call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(
address target,
bytes memory data,
string memory errorMessage
) internal view returns (bytes memory) {
(bool success, bytes memory returndata) = target.staticcall(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.4._
*/
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
return functionDelegateCall(target, data, "Address: low-level delegate call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.4._
*/
function functionDelegateCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
(bool success, bytes memory returndata) = target.delegatecall(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
/**
* @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling
* the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract.
*
* _Available since v4.8._
*/
function verifyCallResultFromTarget(
address target,
bool success,
bytes memory returndata,
string memory errorMessage
) internal view returns (bytes memory) {
if (success) {
if (returndata.length == 0) {
// only check isContract if the call was successful and the return data is empty
// otherwise we already know that it was a contract
require(isContract(target), "Address: call to non-contract");
}
return returndata;
} else {
_revert(returndata, errorMessage);
}
}
/**
* @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the
* revert reason or using the provided one.
*
* _Available since v4.3._
*/
function verifyCallResult(
bool success,
bytes memory returndata,
string memory errorMessage
) internal pure returns (bytes memory) {
if (success) {
return returndata;
} else {
_revert(returndata, errorMessage);
}
}
function _revert(bytes memory returndata, string memory errorMessage) private pure {
// Look for revert reason and bubble it up if present
if (returndata.length > 0) {
// The easiest way to bubble the revert reason is using memory via assembly
/// @solidity memory-safe-assembly
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert(errorMessage);
}
}
}

View File

@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)
pragma solidity ^0.8.19;
pragma solidity ^0.8.0;
/**
* @dev Provides information about the current execution context, including the
@@ -17,4 +17,8 @@ abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
}

View File

@@ -1,14 +1,23 @@
// SPDX-License-Identifier: LGPLv3
pragma solidity ^0.8.19;
import {IERC20} from "./IERC20.sol";
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 {
// Swap state is PENDING when the swap is first created and funded
// Alice sets Stage to READY when she sees the funds locked on the other chain.
// this prevents Bob from withdrawing funds without locking funds on the other chain first
// Stage is set to COMPLETED upon the swap value being claimed or refunded.
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,
@@ -16,46 +25,55 @@ contract SwapCreator is Secp256k1 {
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 {
// the swap initiator, Alice
// address allowed to refund the ether for this 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;
// address allowed to claim the ether for this swap, Bob
// 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;
// the keccak256 hash of the expected public key derived from the secret `s_b`.
// this public key is a point on the secp256k1 curve
bytes32 pubKeyClaim;
// the keccak256 hash of the expected public key derived from the secret `s_a`.
// this public key is a point on the secp256k1 curve
bytes32 pubKeyRefund;
// timestamp before which Alice can call either `setReady` or `refund`
// 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;
// timestamp after which Bob cannot claim, only Alice can refund
// timeout2 is the block timestamp after which Bob cannot claim, only
// Alice can refund.
uint256 timeout2;
// the asset being swapped: equal to address(0) for ETH, or an ERC-20 token address
// asset is address(0) for EVM native currency swaps, or it is the
// address of the token that Alice is providing.
address asset;
// the value of this swap
// value is the wei or token unit amount that Alice locked in the contract
uint256 value;
// choose random
// nonce is a random value chosen by Alice
uint256 nonce;
}
// RelaySwap contains additional information required for relayed transactions.
// This entire structure is encoded and signed by the swap claimer, and the signature is
// passed to the `claimRelayer` function.
// 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 {
// the swap the transaction is for
// swap specifies which swap is being claimed
Swap swap;
// the fee, in wei, paid to the relayer
// fee is the wei amount paid to the relayer
uint256 fee;
// hash of (relayer's payout address || 4-byte salt)
// relayerHash Keccak-256 hash of (relayer's payout address || 4-byte salt)
bytes32 relayerHash;
// address of the swap contract this transaction is meant for
// swapCreator is the address of the swap's contract
address swapCreator;
}
mapping(bytes32 => Stage) public swaps;
event New(
bytes32 swapID,
bytes32 claimKey,
@@ -69,68 +87,86 @@ contract SwapCreator is Secp256k1 {
event Claimed(bytes32 indexed swapID, bytes32 indexed s);
event Refunded(bytes32 indexed swapID, bytes32 indexed s);
// returned when trying to initiate a swap with a zero value
// thrown when the value parameter to `newSwap` is zero
error ZeroValue();
// returned when the pubKeyClaim or pubKeyRefund parameters for `newSwap` are zero
// thrown when either of the claimCommitment or refundCommitment parameters
// passed to `newSwap` are zero
error InvalidSwapKey();
// returned when the claimer parameter for `newSwap` is the zero address
// thrown when the claimer parameter for `newSwap` is the zero address
error InvalidClaimer();
// returned when the timeout1 or timeout2 parameters for `newSwap` are zero
// thrown when the timeout1 or timeout2 parameters for `newSwap` are zero
error InvalidTimeout();
// returned when the ether sent with a `newSwap` transaction does not match the value parameter
// thrown when msg.value of a `newSwap` transaction has the wrong value
error InvalidValue();
// returned when trying to initiate a swap with an ID that already exists
// thrown when trying to initiate a swap with an ID that already exists
error SwapAlreadyExists();
// returned when trying to call `setReady` on a swap that is not in the PENDING stage
// thrown when trying to call `setReady` on a swap that is not in the
// PENDING stage
error SwapNotPending();
// returned when the caller of `setReady` or `refund` is not the swap owner
// thrown when the caller of `setReady` or `refund` is not the swap owner
error OnlySwapOwner();
// returned when the signer of the relayed transaction is not the swap's claimer
// thrown when the signer of the relayed transaction is not the swap's
// claimer
error OnlySwapClaimer();
// returned when trying to call `claim` or `refund` on an invalid swap
// thrown when trying to call `claim` or `refund` on an invalid swap
error InvalidSwap();
// returned when trying to call `claim` or `refund` on a swap that's already completed
// thrown when trying to call `claim` or `refund` on a swap that's already
// completed
error SwapCompleted();
// returned when trying to call `claim` on a swap that's not set to ready or the first timeout has not been reached
// 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();
// returned when trying to call `claim` on a swap where the second timeout has been reached
// thrown when trying to call `claim` on a swap where the second timeout has
// been reached
error TooLateToClaim();
// returned when it's the counterparty's turn to claim and refunding is not allowed
// thrown when it's the counterparty's turn to claim and refunding is not
// allowed
error NotTimeToRefund();
// returned when the provided secret does not match the expected public key
// thrown when the provided secret does not match its expected public key
// hash
error InvalidSecret();
// returned when the signature of a `RelaySwap` is invalid
// thrown when the signature of a `RelaySwap` is invalid
error InvalidSignature();
// returned when the SwapCreator address is a `RelaySwap` is not the addres of this contract
// thrown when the SwapCreator address is a `RelaySwap` is not the address
// of this contract
error InvalidContractAddress();
// returned when the hash of the relayer address and salt passed to `claimRelayer`
// does not match the relayer hash in `RelaySwap`
// 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 with the given parameters.
// it returns the swap's ID.
// _timeoutDuration0: duration between the current timestamp and timeout1
// _timeoutDuration1: duration between timeout1 and timeout2
// `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 _pubKeyClaim,
bytes32 _pubKeyRefund,
bytes32 _claimCommitment,
bytes32 _refundCommitment,
address payable _claimer,
uint256 _timeoutDuration1,
uint256 _timeoutDuration2,
@@ -142,19 +178,19 @@ contract SwapCreator is Secp256k1 {
if (_asset == address(0)) {
if (_value != msg.value) revert InvalidValue();
} else {
// transfer ERC-20 token into this contract
// transfer the token amount to this contract
// WARN: fee-on-transfer tokens are not supported
IERC20(_asset).transferFrom(msg.sender, address(this), _value);
IERC20(_asset).safeTransferFrom(msg.sender, address(this), _value);
}
if (_pubKeyClaim == 0 || _pubKeyRefund == 0) revert InvalidSwapKey();
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),
pubKeyClaim: _pubKeyClaim,
pubKeyRefund: _pubKeyRefund,
claimCommitment: _claimCommitment,
refundCommitment: _refundCommitment,
claimer: _claimer,
timeout1: block.timestamp + _timeoutDuration1,
timeout2: block.timestamp + _timeoutDuration1 + _timeoutDuration2,
@@ -165,13 +201,13 @@ contract SwapCreator is Secp256k1 {
bytes32 swapID = keccak256(abi.encode(swap));
// make sure this isn't overriding an existing swap
// ensure that we are not overriding an existing swap
if (swaps[swapID] != Stage.INVALID) revert SwapAlreadyExists();
emit New(
swapID,
_pubKeyClaim,
_pubKeyRefund,
_claimCommitment,
_refundCommitment,
swap.timeout1,
swap.timeout2,
swap.asset,
@@ -181,7 +217,8 @@ contract SwapCreator is Secp256k1 {
return swapID;
}
// Alice should call setReady() before timeout1 once she verifies the XMR has been locked
// 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();
@@ -190,26 +227,28 @@ contract SwapCreator is Secp256k1 {
emit Ready(swapID);
}
// Bob can call claim if either of these hold true:
// 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 timeout0 and 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);
// send ether to swap claimer
if (_swap.asset == address(0)) {
// Transfer the swap value as the EVM's native currency
_swap.claimer.transfer(_swap.value);
} 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(_swap.asset).transfer(_swap.claimer, _swap.value);
// 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
// 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
@@ -241,11 +280,11 @@ contract SwapCreator is Secp256k1 {
// 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).transfer(
IERC20(_relaySwap.swap.asset).safeTransfer(
_relaySwap.swap.claimer,
_relaySwap.swap.value - _relaySwap.fee
);
IERC20(_relaySwap.swap.asset).transfer(_relayer, _relaySwap.fee);
IERC20(_relaySwap.swap.asset).safeTransfer(_relayer, _relaySwap.fee);
}
}
@@ -257,14 +296,14 @@ contract SwapCreator is Secp256k1 {
if (block.timestamp < _swap.timeout1 && swapStage != Stage.READY) revert TooEarlyToClaim();
if (block.timestamp >= _swap.timeout2) revert TooLateToClaim();
verifySecret(_secret, _swap.pubKeyClaim);
verifySecret(_secret, _swap.claimCommitment);
emit Claimed(swapID, _secret);
swaps[swapID] = Stage.COMPLETED;
}
// Alice can claim a refund:
// - Until timeout1 unless she calls setReady
// - After timeout2
// 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];
@@ -276,7 +315,7 @@ contract SwapCreator is Secp256k1 {
(block.timestamp > _swap.timeout1 || swapStage == Stage.READY)
) revert NotTimeToRefund();
verifySecret(_secret, _swap.pubKeyRefund);
verifySecret(_secret, _swap.refundCommitment);
emit Refunded(swapID, _secret);
// send asset back to swap owner
@@ -284,7 +323,7 @@ contract SwapCreator is Secp256k1 {
if (_swap.asset == address(0)) {
_swap.owner.transfer(_swap.value);
} else {
IERC20(_swap.asset).transfer(_swap.owner, _swap.value);
IERC20(_swap.asset).safeTransfer(_swap.owner, _swap.value);
}
}

View File

@@ -2,7 +2,7 @@
pragma solidity ^0.8.0;
import {ERC20} from "./ERC20.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// ERC20 token for testing purposes
contract TestERC20 is ERC20 {
@@ -31,6 +31,25 @@ contract TestERC20 is ERC20 {
_burn(account, amount);
}
function approve(address spender, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
// This next checks is performed by the USDT contract, that we want to
// be compatible with:
// https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7#code
//
// To change the approve amount you first have to reduce the addresses
// allowance to zero to prevent an attack described here:
// https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/edit
require(
amount == 0 || allowance(owner, spender) == 0,
"approve allowance must be set to zero before updating"
);
_approve(owner, spender, amount);
return true;
}
function transferInternal(address from, address to, uint256 value) public {
_transfer(from, to, value);
}

File diff suppressed because one or more lines are too long

View File

@@ -301,6 +301,14 @@ func (c *ethClient) LatestBlockTimestamp(ctx context.Context) (time.Time, error)
return time.Unix(int64(hdr.Time), 0), nil
}
// Lock is used for 2 purposes:
// 1. Nonce synchronization: Any time you do a transaction, the lock should be
// grabbed before sending the transaction to the mempool and not released until
// after the transaction has been mined into a block (receipt is returned).
// 2. Transactions that must be done together atomically. In our case, the token
// approve(...) call and TransferFrom(...) call made by the SwapCreator
// contract must be performed as one atomic unit without another `approve` call
// made in the middle.
func (c *ethClient) Lock() {
c.mu.Lock()
}

File diff suppressed because one or more lines are too long

View File

@@ -15,29 +15,29 @@ import (
// swap is the same as the auto-generated SwapCreatorSwap type, but with some type
// adjustments and annotations for JSON marshalling.
type swap struct {
Owner common.Address `json:"owner" validate:"required"`
Claimer common.Address `json:"claimer" validate:"required"`
PubKeyClaim types.Hash `json:"pubKeyClaim" validate:"required"`
PubKeyRefund types.Hash `json:"pubKeyRefund" validate:"required"`
Timeout1 *big.Int `json:"timeout1" validate:"required"`
Timeout2 *big.Int `json:"timeout2" validate:"required"`
Asset common.Address `json:"asset"`
Value *big.Int `json:"value" validate:"required"`
Nonce *big.Int `json:"nonce" validate:"required"`
Owner common.Address `json:"owner" validate:"required"`
Claimer common.Address `json:"claimer" validate:"required"`
ClaimCommitment types.Hash `json:"claimCommitment" validate:"required"`
RefundCommitment types.Hash `json:"refundCommitment" validate:"required"`
Timeout1 *big.Int `json:"timeout1" validate:"required"`
Timeout2 *big.Int `json:"timeout2" validate:"required"`
Asset common.Address `json:"asset"`
Value *big.Int `json:"value" validate:"required"`
Nonce *big.Int `json:"nonce" validate:"required"`
}
// MarshalJSON provides JSON marshalling for SwapCreatorSwap
func (sfs *SwapCreatorSwap) MarshalJSON() ([]byte, error) {
return vjson.MarshalStruct(&swap{
Owner: sfs.Owner,
Claimer: sfs.Claimer,
PubKeyClaim: sfs.PubKeyClaim,
PubKeyRefund: sfs.PubKeyRefund,
Timeout1: sfs.Timeout1,
Timeout2: sfs.Timeout2,
Asset: sfs.Asset,
Value: sfs.Value,
Nonce: sfs.Nonce,
Owner: sfs.Owner,
Claimer: sfs.Claimer,
ClaimCommitment: sfs.ClaimCommitment,
RefundCommitment: sfs.RefundCommitment,
Timeout1: sfs.Timeout1,
Timeout2: sfs.Timeout2,
Asset: sfs.Asset,
Value: sfs.Value,
Nonce: sfs.Nonce,
})
}
@@ -48,15 +48,15 @@ func (sfs *SwapCreatorSwap) UnmarshalJSON(data []byte) error {
return err
}
*sfs = SwapCreatorSwap{
Owner: s.Owner,
Claimer: s.Claimer,
PubKeyClaim: s.PubKeyClaim,
PubKeyRefund: s.PubKeyRefund,
Timeout1: s.Timeout1,
Timeout2: s.Timeout2,
Asset: s.Asset,
Value: s.Value,
Nonce: s.Nonce,
Owner: s.Owner,
Claimer: s.Claimer,
ClaimCommitment: s.ClaimCommitment,
RefundCommitment: s.RefundCommitment,
Timeout1: s.Timeout1,
Timeout2: s.Timeout2,
Asset: s.Asset,
Value: s.Value,
Nonce: s.Nonce,
}
return nil
}

View File

@@ -19,21 +19,21 @@ import (
func TestSwapCreatorSwap_JSON(t *testing.T) {
sf := &SwapCreatorSwap{
Owner: ethcommon.HexToAddress("0xda9dfa130df4de4673b89022ee50ff26f6ea73cf"),
Claimer: ethcommon.HexToAddress("0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"),
PubKeyClaim: ethcommon.HexToHash("0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9"),
PubKeyRefund: ethcommon.HexToHash("0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14"),
Timeout1: big.NewInt(1672531200),
Timeout2: big.NewInt(1672545600),
Asset: ethcommon.HexToAddress("0xdac17f958d2ee523a2206206994597c13d831ec7"),
Value: coins.EtherToWei(apd.New(9876, 0)).BigInt(),
Nonce: big.NewInt(1234),
Owner: ethcommon.HexToAddress("0xda9dfa130df4de4673b89022ee50ff26f6ea73cf"),
Claimer: ethcommon.HexToAddress("0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"),
ClaimCommitment: ethcommon.HexToHash("0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9"),
RefundCommitment: ethcommon.HexToHash("0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14"),
Timeout1: big.NewInt(1672531200),
Timeout2: big.NewInt(1672545600),
Asset: ethcommon.HexToAddress("0xdac17f958d2ee523a2206206994597c13d831ec7"),
Value: coins.EtherToWei(apd.New(9876, 0)).BigInt(),
Nonce: big.NewInt(1234),
}
expectedJSON := `{
"owner": "0xda9dfa130df4de4673b89022ee50ff26f6ea73cf",
"claimer": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8",
"pubKeyClaim": "0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9",
"pubKeyRefund": "0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14",
"claimCommitment": "0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9",
"refundCommitment": "0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14",
"timeout1": 1672531200,
"timeout2": 1672545600,
"asset": "0xdac17f958d2ee523a2206206994597c13d831ec7",

View File

@@ -72,10 +72,10 @@ func testNewSwap(t *testing.T, asset types.EthAsset, erc20Contract *TestERC20) {
owner := crypto.PubkeyToAddress(pk.PublicKey)
claimer := common.EthereumPrivateKeyToAddress(tests.GetMakerTestKey(t))
var pubKeyClaim, pubKeyRefund [32]byte
_, err := rand.Read(pubKeyClaim[:])
var claimCommitment, refundCommitment [32]byte
_, err := rand.Read(claimCommitment[:])
require.NoError(t, err)
_, err = rand.Read(pubKeyRefund[:])
_, err = rand.Read(refundCommitment[:])
require.NoError(t, err)
nonce, err := rand.Prime(rand.Reader, 256)
@@ -91,8 +91,8 @@ func testNewSwap(t *testing.T, asset types.EthAsset, erc20Contract *TestERC20) {
tx, err := swapCreator.NewSwap(
txOpts,
pubKeyClaim,
pubKeyRefund,
claimCommitment,
refundCommitment,
claimer,
defaultTimeoutDuration,
defaultTimeoutDuration,
@@ -127,15 +127,15 @@ func testNewSwap(t *testing.T, asset types.EthAsset, erc20Contract *TestERC20) {
// validate that off-chain swapID calculation matches the on-chain value
swap := SwapCreatorSwap{
Owner: owner,
Claimer: claimer,
PubKeyClaim: pubKeyClaim,
PubKeyRefund: pubKeyRefund,
Timeout1: t1,
Timeout2: t2,
Asset: asset.Address(),
Value: value,
Nonce: nonce,
Owner: owner,
Claimer: claimer,
ClaimCommitment: claimCommitment,
RefundCommitment: refundCommitment,
Timeout1: t1,
Timeout2: t2,
Asset: asset.Address(),
Value: value,
Nonce: nonce,
}
// validate our off-net calculation of the SwapID
@@ -189,15 +189,15 @@ func TestSwapCreator_Claim_vec(t *testing.T) {
require.NoError(t, err)
swap := SwapCreatorSwap{
Owner: addr,
Claimer: addr,
PubKeyClaim: cmt,
PubKeyRefund: dummySwapKey,
Timeout1: t1,
Timeout2: t2,
Asset: ethcommon.Address(types.EthAssetETH),
Value: defaultSwapValue,
Nonce: nonce,
Owner: addr,
Claimer: addr,
ClaimCommitment: cmt,
RefundCommitment: dummySwapKey,
Timeout1: t1,
Timeout2: t2,
Asset: ethcommon.Address(types.EthAssetETH),
Value: defaultSwapValue,
Nonce: nonce,
}
// set contract to Ready
@@ -272,15 +272,15 @@ func testClaim(t *testing.T, asset types.EthAsset, newLogIndex int, value *big.I
require.NoError(t, err)
swap := SwapCreatorSwap{
Owner: addr,
Claimer: addr,
PubKeyClaim: cmt,
PubKeyRefund: dummySwapKey,
Timeout1: t1,
Timeout2: t2,
Asset: asset.Address(),
Value: value,
Nonce: nonce,
Owner: addr,
Claimer: addr,
ClaimCommitment: cmt,
RefundCommitment: dummySwapKey,
Timeout1: t1,
Timeout2: t2,
Asset: asset.Address(),
Value: value,
Nonce: nonce,
}
// ensure we can't claim before setting contract to Ready
@@ -365,15 +365,15 @@ func testRefundBeforeT1(t *testing.T, asset types.EthAsset, erc20Contract *TestE
require.NoError(t, err)
swap := SwapCreatorSwap{
Owner: addr,
Claimer: addr,
PubKeyClaim: dummySwapKey,
PubKeyRefund: cmt,
Timeout1: t1,
Timeout2: t2,
Asset: asset.Address(),
Value: defaultSwapValue,
Nonce: nonce,
Owner: addr,
Claimer: addr,
ClaimCommitment: dummySwapKey,
RefundCommitment: cmt,
Timeout1: t1,
Timeout2: t2,
Asset: asset.Address(),
Value: defaultSwapValue,
Nonce: nonce,
}
// now let's try to refund
@@ -454,15 +454,15 @@ func testRefundAfterT2(t *testing.T, asset types.EthAsset, erc20Contract *TestER
// ensure we can't refund between T1 and T2
<-time.After(time.Until(time.Unix(t1.Int64()+1, 0)))
swap := SwapCreatorSwap{
Owner: addr,
Claimer: addr,
PubKeyClaim: dummySwapKey,
PubKeyRefund: cmt,
Timeout1: t1,
Timeout2: t2,
Asset: asset.Address(),
Value: defaultSwapValue,
Nonce: nonce,
Owner: addr,
Claimer: addr,
ClaimCommitment: dummySwapKey,
RefundCommitment: cmt,
Timeout1: t1,
Timeout2: t2,
Asset: asset.Address(),
Value: defaultSwapValue,
Nonce: nonce,
}
secret := proof.Secret()
@@ -533,15 +533,15 @@ func TestSwapCreator_MultipleSwaps(t *testing.T) {
addrSwap := crypto.PubkeyToAddress(*sc.walletKey.Public().(*ecdsa.PublicKey))
sc.swap = SwapCreatorSwap{
Owner: addrSwap,
Claimer: addrSwap,
PubKeyClaim: res.Secp256k1PublicKey().Keccak256(),
PubKeyRefund: dummySwapKey, // no one calls refund in this test
Timeout1: nil, // timeouts initialised when swap is created
Timeout2: nil,
Asset: ethcommon.Address(types.EthAssetETH),
Value: defaultSwapValue,
Nonce: big.NewInt(int64(i)),
Owner: addrSwap,
Claimer: addrSwap,
ClaimCommitment: res.Secp256k1PublicKey().Keccak256(),
RefundCommitment: dummySwapKey, // no one calls refund in this test
Timeout1: nil, // timeouts initialised when swap is created
Timeout2: nil,
Asset: ethcommon.Address(types.EthAssetETH),
Value: defaultSwapValue,
Nonce: big.NewInt(int64(i)),
}
}
@@ -559,8 +559,8 @@ func TestSwapCreator_MultipleSwaps(t *testing.T) {
auth.Value = sc.swap.Value
tx, err := swapCreator.NewSwap(
auth,
sc.swap.PubKeyClaim,
sc.swap.PubKeyRefund,
sc.swap.ClaimCommitment,
sc.swap.RefundCommitment,
sc.swap.Claimer,
defaultTimeoutDuration,
defaultTimeoutDuration,

View File

@@ -120,8 +120,8 @@ func (s *SwapCreatorRelaySwap) Hash() types.Hash {
args, err := arguments.Pack(
s.Swap.Owner,
s.Swap.Claimer,
s.Swap.PubKeyClaim,
s.Swap.PubKeyRefund,
s.Swap.ClaimCommitment,
s.Swap.RefundCommitment,
s.Swap.Timeout1,
s.Swap.Timeout2,
s.Swap.Asset,
@@ -192,8 +192,8 @@ func (sfs *SwapCreatorSwap) SwapID() types.Hash {
args, err := arguments.Pack(
sfs.Owner,
sfs.Claimer,
sfs.PubKeyClaim,
sfs.PubKeyRefund,
sfs.ClaimCommitment,
sfs.RefundCommitment,
sfs.Timeout1,
sfs.Timeout2,
sfs.Asset,

View File

@@ -84,7 +84,7 @@ func (h *Host) receiveInitiateResponse(stream libp2pnetwork.Stream, s SwapState)
err := s.HandleProtocolMessage(msg)
if err != nil {
log.Warnf("failed to handle protocol message: err=%s", err)
log.Errorf("failed to handle protocol message: %s", err)
return
}
case <-time.After(initiateResponseTimeout):

View File

@@ -59,15 +59,15 @@ func createTestClaimRequest() *message.RelayClaimRequest {
sig := [65]byte{0x1}
swap := contracts.SwapCreatorSwap{
Owner: ethcommon.Address{0x1},
Claimer: ethcommon.Address{0x1},
PubKeyClaim: [32]byte{0x1},
PubKeyRefund: [32]byte{0x1},
Timeout1: big.NewInt(time.Now().Add(30 * time.Minute).Unix()),
Timeout2: big.NewInt(time.Now().Add(60 * time.Minute).Unix()),
Asset: ethcommon.Address(types.EthAssetETH),
Value: big.NewInt(1e18),
Nonce: big.NewInt(1),
Owner: ethcommon.Address{0x1},
Claimer: ethcommon.Address{0x1},
ClaimCommitment: [32]byte{0x1},
RefundCommitment: [32]byte{0x1},
Timeout1: big.NewInt(time.Now().Add(30 * time.Minute).Unix()),
Timeout2: big.NewInt(time.Now().Add(60 * time.Minute).Unix()),
Asset: ethcommon.Address(types.EthAssetETH),
Value: big.NewInt(1e18),
Nonce: big.NewInt(1),
}
req := &message.RelayClaimRequest{

View File

@@ -111,22 +111,23 @@ func (s *ExternalSender) approve(
// NewSwap prompts the external sender to sign a new_swap transaction
func (s *ExternalSender) NewSwap(
pubKeyClaim [32]byte,
pubKeyRefund [32]byte,
claimCommitment [32]byte,
refundCommitment [32]byte,
claimer ethcommon.Address,
timeoutDuration *big.Int,
nonce *big.Int,
amount coins.EthAssetAmount,
) (ethcommon.Hash, error) {
saveNewSwapTxCallback func(txHash ethcommon.Hash) error,
) (*ethtypes.Receipt, error) {
// TODO: Add ERC20 token support and approve new_swap for the token transfer
if amount.IsToken() {
return ethcommon.Hash{}, errors.New("external sender does not support ERC20 token swaps")
return nil, errors.New("external sender does not support ERC20 token swaps")
}
input, err := s.abi.Pack("new_swap", pubKeyClaim, pubKeyRefund, claimer, timeoutDuration,
input, err := s.abi.Pack("new_swap", claimCommitment, refundCommitment, claimer, timeoutDuration,
amount.TokenAddress(), amount.BigInt(), nonce)
if err != nil {
return ethcommon.Hash{}, err
return nil, err
}
tx := &Transaction{
@@ -142,11 +143,15 @@ func (s *ExternalSender) NewSwap(
var txHash ethcommon.Hash
select {
case <-time.After(transactionTimeout):
return ethcommon.Hash{}, errTransactionTimeout
return nil, errTransactionTimeout
case txHash = <-s.in:
}
return txHash, nil
if err := saveNewSwapTxCallback(txHash); err != nil {
return nil, err
}
return block.WaitForReceipt(s.ctx, s.ec, txHash)
}
// SetReady prompts the external sender to sign a set_ready transaction
@@ -199,10 +204,5 @@ func (s *ExternalSender) sendAndReceive(input []byte, to ethcommon.Address) (*et
case txHash = <-s.in:
}
receipt, err := block.WaitForReceipt(s.ctx, s.ec, txHash)
if err != nil {
return nil, err
}
return receipt, nil
return block.WaitForReceipt(s.ctx, s.ec, txHash)
}

View File

@@ -10,9 +10,12 @@ package txsender
import (
"context"
"errors"
"fmt"
"math/big"
"github.com/cockroachdb/apd/v3"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
logging "github.com/ipfs/go-log"
@@ -33,13 +36,14 @@ type Sender interface {
SetSwapCreator(*contracts.SwapCreator)
SetSwapCreatorAddr(ethcommon.Address)
NewSwap(
pubKeyClaim [32]byte,
pubKeyRefund [32]byte,
claimCommitment [32]byte,
refundCommitment [32]byte,
claimer ethcommon.Address,
timeoutDuration *big.Int,
nonce *big.Int,
amount coins.EthAssetAmount,
) (ethcommon.Hash, error)
saveNewSwapTxCallback func(txHash ethcommon.Hash) error,
) (*ethtypes.Receipt, error)
SetReady(swap *contracts.SwapCreatorSwap) (*ethtypes.Receipt, error)
Claim(swap *contracts.SwapCreatorSwap, secret [32]byte) (*ethtypes.Receipt, error)
Refund(swap *contracts.SwapCreatorSwap, secret [32]byte) (*ethtypes.Receipt, error)
@@ -77,45 +81,32 @@ func (s *privateKeySender) SetSwapCreator(contract *contracts.SwapCreator) {
func (s *privateKeySender) SetSwapCreatorAddr(_ ethcommon.Address) {}
func (s *privateKeySender) NewSwap(
pubKeyClaim [32]byte,
pubKeyRefund [32]byte,
claimCommitment [32]byte,
refundCommitment [32]byte,
claimer ethcommon.Address,
timeoutDuration *big.Int,
nonce *big.Int,
amount coins.EthAssetAmount,
) (ethcommon.Hash, error) {
// note: the caller must lock the ethclient.
saveNewSwapTxCallback func(txHash ethcommon.Hash) error,
) (*ethtypes.Receipt, error) {
// For token swaps, approving our contract to transfer tokens, and calling
// NewSwap which performs the transfer, need to be inside the same wallet
// lock grab in case there are other simultaneous swaps happening with the
// same token.
s.ethClient.Lock()
defer s.ethClient.Unlock()
value := amount.BigInt()
// For token swaps, approving our contract to transfer tokens, and calling
// NewSwap which performs the transfer, needs to be inside the same wallet
// lock grab in case there are other simultaneous swaps happening with the
// same token.
if amount.IsToken() {
txOpts, err := s.ethClient.TxOpts(s.ctx)
if err != nil {
return ethcommon.Hash{}, err
if err := s.approveTransferFrom(amount); err != nil {
return nil, err
}
tx, err := s.erc20Contract.Approve(txOpts, s.swapCreatorAddr, value)
if err != nil {
return ethcommon.Hash{}, fmt.Errorf("approve tx creation failed, %w", err)
}
receipt, err := block.WaitForReceipt(s.ctx, s.ethClient.Raw(), tx.Hash())
if err != nil {
return ethcommon.Hash{}, fmt.Errorf("approve failed, %w", err)
}
log.Debugf("approve transaction included %s", common.ReceiptInfo(receipt))
log.Infof("%s %s approved for use by SwapCreator's new_swap",
amount.AsStdString(), amount.StdSymbol())
}
txOpts, err := s.ethClient.TxOpts(s.ctx)
if err != nil {
return ethcommon.Hash{}, err
return nil, err
}
// transfer ETH if we're not doing an ERC20 swap
@@ -123,14 +114,108 @@ func (s *privateKeySender) NewSwap(
txOpts.Value = value
}
tx, err := s.swapCreator.NewSwap(txOpts, pubKeyClaim, pubKeyRefund, claimer, timeoutDuration, timeoutDuration,
tx, err := s.swapCreator.NewSwap(txOpts, claimCommitment, refundCommitment, claimer, timeoutDuration, timeoutDuration,
amount.TokenAddress(), value, nonce)
if err != nil {
err = fmt.Errorf("new_swap tx creation failed, %w", err)
return ethcommon.Hash{}, err
return nil, err
}
return tx.Hash(), nil
if err = saveNewSwapTxCallback(tx.Hash()); err != nil {
return nil, err
}
receipt, err := block.WaitForReceipt(s.ctx, s.ethClient.Raw(), tx.Hash())
if err != nil {
return nil, fmt.Errorf("NewSwap tx %s failed waiting for receipt, %w", tx.Hash(), err)
}
log.Infof("newSwap TX succeeded, %s", common.ReceiptInfo(receipt))
return receipt, nil
}
// approveTransferFrom grants the SwapCreator contract permission to transfer
// the passed amount of tokens. Since the transfer happens inside the NewSwap
// transaction, we need to grant the contract's address approval for the
// transfer amount first. The ethClient lock should already have been grabbed
// before invoking this method.
func (s *privateKeySender) approveTransferFrom(amount coins.EthAssetAmount) error {
if !amount.IsToken() {
panic("this function should only be called with a token asset")
}
if amount.AsStd().IsZero() {
return errors.New("approveContractToTransfer can not be called with a zero amount")
}
balance, err := s.ethClient.ERC20Balance(s.ctx, amount.TokenAddress())
if err != nil {
return err
}
if balance.AsStd().Cmp(amount.AsStd()) < 0 {
return fmt.Errorf("balance of %s %s is under the %s swap amount",
balance.AsStdString(), balance.StdSymbol(), amount.AsStdString())
}
token := balance.TokenInfo
// Make a free call to the Allowance function to determine if we need to spend
// gas approving the SwapCreator contract to transfer the token.
bindOpts := &bind.CallOpts{Context: s.ctx}
allowedAmtBI, err := s.erc20Contract.Allowance(bindOpts, s.ethClient.Address(), s.swapCreatorAddr)
if err != nil {
return err
}
allowedAmt := coins.NewERC20TokenAmountFromBigInt(allowedAmtBI, token)
if amount.AsStd().Cmp(allowedAmt.AsStd()) <= 0 {
log.Infof("swapCreator was already approved to transfer %s %s (needed %s)",
allowedAmt.AsStdString(), allowedAmt.StdSymbol(), amount.AsStdString())
return nil
}
// If the previous approved amount is not zero, some ERC20 contracts like
// USDT require us to first zero out the approved amount before setting
// a new value.
if !allowedAmt.AsStd().IsZero() {
log.Debugf("zeroing approved token amount before raising limit from %s to %s %s",
allowedAmt.AsStdString(), amount.AsStdString(), amount.StdSymbol())
zeroTokenAmt := coins.NewTokenAmountFromDecimals(new(apd.Decimal), token)
if err = s.approveNoChecks(zeroTokenAmt); err != nil {
return err
}
}
return s.approveNoChecks(amount)
}
// approveNoChecks is a helper method to gives the SwapCreator contract
// permission to transfer the passed-in amount of tokens. It's caller should be
// doing other checks and have already grabbed the the ethClient's lock.
func (s *privateKeySender) approveNoChecks(amount coins.EthAssetAmount) error {
txOpts, err := s.ethClient.TxOpts(s.ctx)
if err != nil {
return err
}
tx, err := s.erc20Contract.Approve(txOpts, s.swapCreatorAddr, amount.BigInt())
if err != nil {
return fmt.Errorf("token approve tx for %s %s creation failed, %w",
amount.AsStdString(), amount.StdSymbol(), err)
}
receipt, err := block.WaitForReceipt(s.ctx, s.ethClient.Raw(), tx.Hash())
if err != nil {
return fmt.Errorf("approveNoChecks tx %s failed waiting for receipt, %w", tx.Hash(), err)
}
log.Infof("%s %s approved for use by SwapCreator's new_swap, %s",
amount.AsStdString(), amount.StdSymbol(), common.ReceiptInfo(receipt))
return nil
}
func (s *privateKeySender) SetReady(swap *contracts.SwapCreatorSwap) (*ethtypes.Receipt, error) {

View File

@@ -0,0 +1,84 @@
package txsender
import (
"context"
"testing"
"github.com/cockroachdb/apd/v3"
"github.com/stretchr/testify/require"
"github.com/athanorlabs/atomic-swap/cliutil"
"github.com/athanorlabs/atomic-swap/coins"
contracts "github.com/athanorlabs/atomic-swap/ethereum"
"github.com/athanorlabs/atomic-swap/ethereum/extethclient"
"github.com/athanorlabs/atomic-swap/tests"
)
func init() {
cliutil.SetLogLevels("debug")
}
func newTestSender(t *testing.T) (*privateKeySender, *coins.ERC20TokenInfo) {
ctx := context.Background()
pk := tests.GetTakerTestKey(t)
ec := extethclient.CreateTestClient(t, pk)
swapCreatorAddr, swapCreator := contracts.DevDeploySwapCreator(t, ec.Raw(), pk)
token := contracts.GetMockTether(t, ec.Raw(), pk)
tokenBinding, err := contracts.NewIERC20(token.Address, ec.Raw())
require.NoError(t, err)
sender := NewSenderWithPrivateKey(ctx, ec, swapCreatorAddr, swapCreator, tokenBinding)
return sender.(*privateKeySender), token
}
// Verify that our test token behaves like Tether in that changing the approval amount
// must be set to zero before it can be changed to a non-zero value.
func Test_privateKeySender_approve(t *testing.T) {
sender, token := newTestSender(t)
zeroAmt := coins.NewTokenAmountFromDecimals(new(apd.Decimal), token)
tooMuchAmt := coins.NewTokenAmountFromDecimals(coins.StrToDecimal("1000000"), token)
// Ensure this fails, approving zero amounts should be done using approveNoChecks
err := sender.approveTransferFrom(zeroAmt)
require.ErrorContains(t, err, "can not be called with a zero amount")
// Ensure our balance check failure works
err = sender.approveTransferFrom(tooMuchAmt)
require.ErrorContains(t, err, " is under ")
// Make sure that we always start testing in a know state, where the swapCreator
// contract is not approved to transfer any token amount
err = sender.approveNoChecks(zeroAmt)
require.NoError(t, err)
// First approve succeeds, as the current allowance is zero
amt := coins.NewTokenAmountFromDecimals(coins.StrToDecimal("3"), token)
err = sender.approveTransferFrom(amt)
require.NoError(t, err)
// Second approve succeeds, as we are already approved for more than is being
// asked for. This is an attempt to optimize gas, but it should happen very
// often. The only way you can easily get into this state is if a previous
// swap failed after approve but before a successful NewSwap transaction.
amt = coins.NewTokenAmountFromDecimals(coins.StrToDecimal("2"), token)
err = sender.approveTransferFrom(amt)
require.NoError(t, err)
// Third approve fails, because we are trying to increase the approval
// amount without zeroing it first. Contracts like Tether do not allow this,
// as it gives the contract being granted approval the potential to quickly
// transfer the previously approved amount before the new approval gets
// mined and then do a second transfer using the new approved amount.
amt = coins.NewTokenAmountFromDecimals(coins.StrToDecimal("4"), token)
err = sender.approveNoChecks(amt)
require.ErrorContains(t, err, `token approve tx for 4 "USDT" creation failed`)
// The next approval, of the same amount that just failed, is actually 2
// approvals. When calling approveTransferFrom, the code will see that the
// approval amount needs to be raised and zero it out before raising it.
err = sender.approveTransferFrom(amt)
require.NoError(t, err)
}

View File

@@ -101,15 +101,15 @@ func newTestSwap(
require.NoError(t, err)
contractSwap := &contracts.SwapCreatorSwap{
Owner: ethAddr,
Claimer: ethAddr,
PubKeyClaim: claimKey,
PubKeyRefund: refundKey,
Timeout1: t1,
Timeout2: t2,
Asset: ethcommon.Address(asset),
Value: amount,
Nonce: nonce,
Owner: ethAddr,
Claimer: ethAddr,
ClaimCommitment: claimKey,
RefundCommitment: refundKey,
Timeout1: t1,
Timeout2: t2,
Asset: ethcommon.Address(asset),
Value: amount,
Nonce: nonce,
}
return contractSwap, contractSwapID, tx.Hash()

View File

@@ -195,15 +195,15 @@ func (inst *Instance) refundOrCancelNewSwap(s *swap.Info, txHash ethcommon.Hash)
}
swap := contracts.SwapCreatorSwap{
Owner: params.owner,
Claimer: params.claimer,
PubKeyClaim: params.cmtXMRMaker,
PubKeyRefund: params.cmtXMRTaker,
Timeout1: t1,
Timeout2: t2,
Asset: params.asset,
Value: params.value,
Nonce: params.nonce,
Owner: params.owner,
Claimer: params.claimer,
ClaimCommitment: params.cmtXMRMaker,
RefundCommitment: params.cmtXMRTaker,
Timeout1: t1,
Timeout2: t2,
Asset: params.asset,
Value: params.value,
Nonce: params.nonce,
}
// our secret value
@@ -464,8 +464,8 @@ func getNewSwapParametersFromTx(
}
claimer := newSwapInputs["_claimer"].(ethcommon.Address)
cmtXMRMaker := newSwapInputs["_pubKeyClaim"].([32]byte)
cmtXMRTaker := newSwapInputs["_pubKeyRefund"].([32]byte)
cmtXMRMaker := newSwapInputs["_claimCommitment"].([32]byte)
cmtXMRTaker := newSwapInputs["_refundCommitment"].([32]byte)
asset := newSwapInputs["_asset"].(ethcommon.Address)
value := newSwapInputs["_value"].(*big.Int)
nonce := newSwapInputs["_nonce"].(*big.Int)

View File

@@ -107,7 +107,7 @@ func Test_validateMinBalForTokenSwap_InsufficientETHBalance(t *testing.T) {
// Amount in check below is truncated, so minor adjustments in the
// expected gas won't break the test. The full message looks like:
// "balance of 0.007 ETH is under required amount of 0.00743302 ETH"
require.ErrorContains(t, err, `balance of 0.007 ETH is under required amount of 0.0074`)
require.ErrorContains(t, err, `balance of 0.007 ETH is under required amount of 0.0075`)
}
func Test_validateMinBalanceETH(t *testing.T) {

View File

@@ -24,7 +24,6 @@ import (
"github.com/athanorlabs/atomic-swap/db"
"github.com/athanorlabs/atomic-swap/dleq"
contracts "github.com/athanorlabs/atomic-swap/ethereum"
"github.com/athanorlabs/atomic-swap/ethereum/block"
"github.com/athanorlabs/atomic-swap/ethereum/watcher"
"github.com/athanorlabs/atomic-swap/monero"
"github.com/athanorlabs/atomic-swap/net/message"
@@ -607,7 +606,7 @@ func (s *swapState) lockAsset() (*ethtypes.Receipt, error) {
nonce := contracts.GenerateNewSwapNonce()
receipt, err := s.lockAndWaitForReceipt(cmtXMRMaker, cmtXMRTaker, nonce)
if err != nil {
return nil, fmt.Errorf("failed to lock asset: %w", err)
return nil, err
}
log.Infof("instantiated swap on-chain: amount=%s asset=%s %s",
@@ -643,15 +642,15 @@ func (s *swapState) lockAsset() (*ethtypes.Receipt, error) {
s.setTimeouts(t1, t2)
s.contractSwap = &contracts.SwapCreatorSwap{
Owner: s.ETHClient().Address(),
Claimer: s.xmrmakerAddress,
PubKeyClaim: cmtXMRMaker,
PubKeyRefund: cmtXMRTaker,
Timeout1: t1,
Timeout2: t2,
Asset: ethcommon.Address(s.info.EthAsset),
Value: s.providedAmount.BigInt(),
Nonce: nonce,
Owner: s.ETHClient().Address(),
Claimer: s.xmrmakerAddress,
ClaimCommitment: cmtXMRMaker,
RefundCommitment: cmtXMRTaker,
Timeout1: t1,
Timeout2: t2,
Asset: ethcommon.Address(s.info.EthAsset),
Value: s.providedAmount.BigInt(),
Nonce: nonce,
}
ethInfo := &db.EthereumSwapInfo{
@@ -673,31 +672,33 @@ func (s *swapState) lockAndWaitForReceipt(
cmtXMRMaker, cmtXMRTaker [32]byte,
nonce *big.Int,
) (*ethtypes.Receipt, error) {
s.Backend.ETHClient().Lock()
defer s.Backend.ETHClient().Unlock()
txHash, err := s.sender.NewSwap(
// We need a callback to save the TX hash, as we've seen NewSwap
// transactions stay in the mempool for hours before getting included in a
// block. We want to ensure that the NewSwap TX hash gets stored to the
// database even if the NewSwap function call returns an error, because it
// failed to get a TX receipt.
saveNewSwapTxCallback := func(txHash ethcommon.Hash) error {
err := s.Backend.RecoveryDB().PutNewSwapTxHash(s.OfferID(), txHash)
if err != nil {
return fmt.Errorf("failed to write newSwap tx hash to db: %w", err)
}
return nil
}
receipt, err := s.sender.NewSwap(
cmtXMRMaker,
cmtXMRTaker,
s.xmrmakerAddress,
big.NewInt(int64(s.SwapTimeout().Seconds())),
nonce,
s.providedAmount,
saveNewSwapTxCallback,
)
if err != nil {
return nil, fmt.Errorf("failed to instantiate swap on-chain: %w", err)
}
err = s.Backend.RecoveryDB().PutNewSwapTxHash(s.OfferID(), txHash)
if err != nil {
return nil, fmt.Errorf("failed to write newSwap tx hash to db: %w", err)
}
receipt, err := block.WaitForReceipt(s.ctx, s.ETHClient().Raw(), txHash)
if err != nil {
return nil, fmt.Errorf("failed to get newSwap transaction receipt: %w", err)
}
return receipt, nil
}

View File

@@ -20,15 +20,15 @@ import (
func createTestSwap(claimer ethcommon.Address) *contracts.SwapCreatorSwap {
return &contracts.SwapCreatorSwap{
Owner: ethcommon.Address{0x1},
Claimer: claimer,
PubKeyClaim: [32]byte{0x1},
PubKeyRefund: [32]byte{0x1},
Timeout1: big.NewInt(time.Now().Add(30 * time.Minute).Unix()),
Timeout2: big.NewInt(time.Now().Add(60 * time.Minute).Unix()),
Asset: ethcommon.Address(types.EthAssetETH),
Value: big.NewInt(1e18),
Nonce: big.NewInt(1),
Owner: ethcommon.Address{0x1},
Claimer: claimer,
ClaimCommitment: [32]byte{0x1},
RefundCommitment: [32]byte{0x1},
Timeout1: big.NewInt(time.Now().Add(30 * time.Minute).Unix()),
Timeout2: big.NewInt(time.Now().Add(60 * time.Minute).Unix()),
Asset: ethcommon.Address(types.EthAssetETH),
Value: big.NewInt(1e18),
Nonce: big.NewInt(1),
}
}

View File

@@ -88,15 +88,15 @@ func Test_ValidateAndSendTransaction(t *testing.T) {
require.NoError(t, err)
swap := contracts.SwapCreatorSwap{
Owner: relayerAddr,
Claimer: claimerAddr,
PubKeyClaim: cmt,
PubKeyRefund: refundKey,
Timeout1: t1,
Timeout2: t2,
Asset: types.EthAssetETH.Address(),
Value: value,
Nonce: nonce,
Owner: relayerAddr,
Claimer: claimerAddr,
ClaimCommitment: cmt,
RefundCommitment: refundKey,
Timeout1: t1,
Timeout2: t2,
Asset: types.EthAssetETH.Address(),
Value: value,
Nonce: nonce,
}
// set contract to Ready

View File

@@ -56,15 +56,15 @@ func TestValidateRelayerFee(t *testing.T) {
for _, tc := range testCases {
swap := contracts.SwapCreatorSwap{
Owner: ethcommon.Address{},
Claimer: ethcommon.Address{},
PubKeyClaim: [32]byte{},
PubKeyRefund: [32]byte{},
Timeout1: new(big.Int),
Timeout2: new(big.Int),
Asset: ethcommon.Address{},
Value: tc.value,
Nonce: new(big.Int),
Owner: ethcommon.Address{},
Claimer: ethcommon.Address{},
ClaimCommitment: [32]byte{},
RefundCommitment: [32]byte{},
Timeout1: new(big.Int),
Timeout2: new(big.Int),
Asset: ethcommon.Address{},
Value: tc.value,
Nonce: new(big.Int),
}
request := &message.RelayClaimRequest{

View File

@@ -1,8 +1,10 @@
#!/usr/bin/env bash
set -e
# Use the project root (one directory above this script) as the current working directory:
PROJECT_ROOT="$(dirname "$(dirname "$(realpath "$0")")")"
cd "${PROJECT_ROOT}" || exit 1
cd "${PROJECT_ROOT}"
ABIGEN="$(go env GOPATH)/bin/abigen"
@@ -11,14 +13,27 @@ if [[ -z "${SOLC_BIN}" ]]; then
fi
compile-contract() {
local solidity_type_name="${1:?}"
local solidity_file_name="${1:?}"
local go_type_name="${2:?}"
local go_file_name="${3:?}"
# strip leading path and extension from to get the solidity type name
local solidity_type_name
solidity_type_name="$(basename "${solidity_file_name%.sol}")"
echo "Generating go bindings for ${solidity_type_name}"
"${SOLC_BIN}" --optimize --optimize-runs=200 --abi "ethereum/contracts/${solidity_type_name}.sol" -o ethereum/abi/ --overwrite
"${SOLC_BIN}" --optimize --optimize-runs=200 --bin "ethereum/contracts/${solidity_type_name}.sol" -o ethereum/bin/ --overwrite
"${SOLC_BIN}" --optimize --optimize-runs=200 \
--metadata --metadata-literal \
--base-path "ethereum/contracts" \
--abi "ethereum/contracts/${solidity_file_name}" \
-o ethereum/abi/ --overwrite
"${SOLC_BIN}" --optimize --optimize-runs=200 \
--base-path ethereum/contracts \
--include-path . \
--bin "ethereum/contracts/${solidity_file_name}" \
-o ethereum/bin/ --overwrite
"${ABIGEN}" \
--abi "ethereum/abi/${solidity_type_name}.abi" \
--bin "ethereum/bin/${solidity_type_name}.bin" \
@@ -27,7 +42,16 @@ compile-contract() {
--out "ethereum/${go_file_name}.go"
}
compile-contract SwapCreator SwapCreator swap_creator
compile-contract TestERC20 TestERC20 erc20_token
compile-contract IERC20Metadata IERC20 ierc20
compile-contract AggregatorV3Interface AggregatorV3Interface aggregator_v3_interface
compile-contract SwapCreator.sol SwapCreator swap_creator
compile-contract TestERC20.sol TestERC20 erc20_token
compile-contract @openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol IERC20 ierc20
compile-contract AggregatorV3Interface.sol AggregatorV3Interface aggregator_v3_interface
# etherscan lets you upload solidity sources in "standard JSON input" format.
# With solc 0.8.20+, using the "--metadata --metadata-literal" flags, we can
# strip out a bunch of fields from the output metadata to get an input file that
# etherscan accepts. etherscan should accept the ".settings.compilationTarget"
# field, but the field was only introduced in solc 0.8.20. Try adding it back
# later.
jq 'del( .output, .compiler, .settings.compilationTarget, .version, .sources[] .license )' \
ethereum/abi/SwapCreator_meta.json >ethereum/abi/SwapCreator_etherscan.json

View File

@@ -47,6 +47,7 @@ var testPackages = []struct {
{"protocol/backend", 2},
{"protocol/xmrmaker", 3},
{"protocol/xmrtaker", 3},
{"protocol/txsender", 2},
{"recover", 2},
{"relayer", 2},
{"tests", 3},
@@ -71,10 +72,10 @@ var ganacheTestKeys = []string{
/* RESERVED KEYS:
* "4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d", // ganache key #0
* "6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1", // ganache key #1
* "6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c", // ganache key #2
* "646f1ce2fdad0e6deeeb5c7e8e5543bdde65e86029e2fd9fc169899c440a7913", // ganache key #3
* "add53f9a7e588d003326d1cbf9e4a43c061aadd9bc938c843a79e7b4fd2ad743", // ganache key #4
*/
"6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c", // ganache key #2
"646f1ce2fdad0e6deeeb5c7e8e5543bdde65e86029e2fd9fc169899c440a7913", // ganache key #3
"add53f9a7e588d003326d1cbf9e4a43c061aadd9bc938c843a79e7b4fd2ad743", // ganache key #4
"395df67f0c2d2d9fe1ad08d1bc8b6627011959b79c53d7dd6a3536a33ab8a4fd", // ganache key #5
"e485d098507f54e7733a205420dfddbe58db035fa577fc294ebd14db90767a52", // ganache key #6
"a453611d9419d0e56f499079478fd72c37b251a94bfde4d19872c44cf65386e3", // ganache key #7