feat: update SwapCreater.sol claimRelayer to no longer use forwarder (#449)

Co-authored-by: Dmitry Holodov <dimalinux@protonmail.com>
This commit is contained in:
noot
2023-05-01 19:23:17 -04:00
committed by GitHub
parent b3dcb96074
commit fb95751dda
45 changed files with 878 additions and 1126 deletions

View File

@@ -31,7 +31,6 @@ var (
type contractAddresses struct {
SwapCreatorAddr ethcommon.Address `json:"swapCreatorAddr" validate:"required"`
ForwarderAddr ethcommon.Address `json:"forwarderAddr" validate:"required"`
}
func getOrDeploySwapCreator(
@@ -40,7 +39,6 @@ func getOrDeploySwapCreator(
env common.Environment,
dataDir string,
ec extethclient.EthClient,
forwarderAddr ethcommon.Address,
) (ethcommon.Address, error) {
var err error
if (swapCreatorAddr == ethcommon.Address{}) {
@@ -49,7 +47,7 @@ func getOrDeploySwapCreator(
time.Sleep(10 * time.Second)
}
swapCreatorAddr, _, err = deploySwapCreator(ctx, ec.Raw(), ec.PrivateKey(), forwarderAddr, dataDir)
swapCreatorAddr, err = deploySwapCreator(ctx, ec.Raw(), ec.PrivateKey(), dataDir)
if err != nil {
return ethcommon.Address{}, fmt.Errorf("failed to deploy swap creator: %w", err)
}
@@ -57,7 +55,7 @@ func getOrDeploySwapCreator(
// otherwise, load the contract from the given address
// and check that its bytecode is valid (ie. matches the
// bytecode of this repo's swap contract)
_, err = contracts.CheckSwapCreatorContractCode(ctx, ec.Raw(), swapCreatorAddr)
err = contracts.CheckSwapCreatorContractCode(ctx, ec.Raw(), swapCreatorAddr)
if err != nil {
return ethcommon.Address{}, err
}
@@ -70,31 +68,15 @@ func deploySwapCreator(
ctx context.Context,
ec *ethclient.Client,
privkey *ecdsa.PrivateKey,
forwarderAddr ethcommon.Address,
dataDir string,
) (ethcommon.Address, *contracts.SwapCreator, error) {
) (ethcommon.Address, error) {
if privkey == nil {
return ethcommon.Address{}, nil, errNoEthPrivateKey
return ethcommon.Address{}, errNoEthPrivateKey
}
if (forwarderAddr == ethcommon.Address{}) {
// deploy forwarder contract as well
var err error
forwarderAddr, err = contracts.DeployGSNForwarderWithKey(ctx, ec, privkey)
if err != nil {
return ethcommon.Address{}, nil, err
}
} else {
// TODO: ignore this if the forwarderAddr is the one that's hardcoded for this network
if err := contracts.CheckForwarderContractCode(ctx, ec, forwarderAddr); err != nil {
return ethcommon.Address{}, nil, err
}
}
swapCreatorAddr, sf, err := contracts.DeploySwapCreatorWithKey(ctx, ec, privkey, forwarderAddr)
swapCreatorAddr, _, err := contracts.DeploySwapCreatorWithKey(ctx, ec, privkey)
if err != nil {
return ethcommon.Address{}, nil, err
return ethcommon.Address{}, err
}
// store the contract addresses on disk
@@ -102,14 +84,13 @@ func deploySwapCreator(
path.Join(dataDir, contractAddressesFile),
&contractAddresses{
SwapCreatorAddr: swapCreatorAddr,
ForwarderAddr: forwarderAddr,
},
)
if err != nil {
return ethcommon.Address{}, nil, fmt.Errorf("failed to write contract address to file: %w", err)
return ethcommon.Address{}, fmt.Errorf("failed to write contract address to file: %w", err)
}
return swapCreatorAddr, sf, nil
return swapCreatorAddr, nil
}
// writeContractAddressesToFile writes the contract addresses to the given file

View File

@@ -8,7 +8,6 @@ import (
"testing"
"github.com/athanorlabs/atomic-swap/common"
contracts "github.com/athanorlabs/atomic-swap/ethereum"
"github.com/athanorlabs/atomic-swap/ethereum/extethclient"
"github.com/athanorlabs/atomic-swap/tests"
@@ -16,26 +15,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestGetOrDeploySwapCreator_DeployNoForwarder(t *testing.T) {
pk := tests.GetTakerTestKey(t)
ec := extethclient.CreateTestClient(t, pk)
tmpDir := t.TempDir()
forwarder, err := contracts.DeployGSNForwarderWithKey(context.Background(), ec.Raw(), pk)
require.NoError(t, err)
_, err = getOrDeploySwapCreator(
context.Background(),
ethcommon.Address{},
common.Development,
tmpDir,
ec,
forwarder,
)
require.NoError(t, err)
}
func TestGetOrDeploySwapCreator_DeployForwarderAlso(t *testing.T) {
func TestGetOrDeploySwapCreator_Deploy(t *testing.T) {
pk := tests.GetTakerTestKey(t)
ec := extethclient.CreateTestClient(t, pk)
tmpDir := t.TempDir()
@@ -46,7 +26,6 @@ func TestGetOrDeploySwapCreator_DeployForwarderAlso(t *testing.T) {
common.Development,
tmpDir,
ec,
ethcommon.Address{},
)
require.NoError(t, err)
}
@@ -56,10 +35,6 @@ func TestGetOrDeploySwapCreator_Get(t *testing.T) {
ec := extethclient.CreateTestClient(t, pk)
tmpDir := t.TempDir()
forwarder, err := contracts.DeployGSNForwarderWithKey(context.Background(), ec.Raw(), pk)
require.NoError(t, err)
t.Log(forwarder)
// deploy and get address
address, err := getOrDeploySwapCreator(
context.Background(),
@@ -67,7 +42,6 @@ func TestGetOrDeploySwapCreator_Get(t *testing.T) {
common.Development,
tmpDir,
ec,
forwarder,
)
require.NoError(t, err)
@@ -77,7 +51,6 @@ func TestGetOrDeploySwapCreator_Get(t *testing.T) {
common.Development,
tmpDir,
ec,
ethcommon.Address{},
)
require.NoError(t, err)
require.Equal(t, address, addr2)

View File

@@ -71,11 +71,10 @@ const (
flagUseExternalSigner = "external-signer"
flagRelayer = "relayer"
flagDevXMRTaker = "dev-xmrtaker"
flagDevXMRMaker = "dev-xmrmaker"
flagDeploy = "deploy"
flagForwarderAddress = "forwarder-address"
flagNoTransferBack = "no-transfer-back"
flagDevXMRTaker = "dev-xmrtaker"
flagDevXMRMaker = "dev-xmrmaker"
flagDeploy = "deploy"
flagNoTransferBack = "no-transfer-back"
flagLogLevel = cliutil.FlagLogLevel
flagProfile = "profile"
@@ -187,10 +186,6 @@ func cliApp() *cli.App {
Name: flagDeploy,
Usage: "Deploy an instance of the swap contract",
},
&cli.StringFlag{
Name: flagForwarderAddress,
Usage: "Ethereum address of the trusted forwarder contract to use when deploying the swap contract",
},
&cli.BoolFlag{
Name: flagNoTransferBack,
Usage: "Leave XMR in generated swap wallet instead of sweeping funds to primary.",
@@ -369,36 +364,18 @@ func validateOrDeployContracts(c *cli.Context, envConf *common.Config, ec exteth
panic("contract address should have been zeroed when envConf was initialized")
}
// forwarderAddr is set only if we're deploying the swap creator contract
// and the --forwarder-address flag is set. Otherwise, if we're deploying
// and this flag isn't set, we deploy both the forwarder and the swap
// creator contracts.
var forwarderAddr ethcommon.Address
forwarderAddrStr := c.String(flagForwarderAddress)
if deploy && forwarderAddrStr != "" {
if !ethcommon.IsHexAddress(forwarderAddrStr) {
return fmt.Errorf("%q requires a valid ethereum address", flagForwarderAddress)
}
forwarderAddr = ethcommon.HexToAddress(forwarderAddrStr)
} else if !deploy && forwarderAddrStr != "" {
return fmt.Errorf("using flag %q requires the %q flag", flagForwarderAddress, flagDeploy)
}
swapCreatorAddr, err := getOrDeploySwapCreator(
c.Context,
envConf.SwapCreatorAddr,
envConf.Env,
envConf.DataDir,
ec,
forwarderAddr,
)
if err != nil {
return err
}
envConf.SwapCreatorAddr = swapCreatorAddr
return nil
}

View File

@@ -9,7 +9,6 @@ import (
"fmt"
"os"
"path"
"strings"
"sync"
"testing"
"time"
@@ -89,8 +88,8 @@ func TestDaemon_DevXMRTaker(t *testing.T) {
}
//
// Validate that --deploy created a contract address file and that we
// deployed a forwarder. At some future point, we will ask the RPC endpoint
// Validate that --deploy created a contract address file.
// At some future point, we will ask the RPC endpoint
// what the contract addresses are instead of using this file.
//
data, err := os.ReadFile(path.Join(dataDir, contractAddressesFile))
@@ -99,19 +98,10 @@ func TestDaemon_DevXMRTaker(t *testing.T) {
require.NoError(t, json.Unmarshal(data, &m))
swapCreatorAddr, ok := m["swapCreatorAddr"]
require.True(t, ok)
forwarderAddr, ok := m["forwarderAddr"]
require.True(t, ok)
ec, _ := tests.NewEthClient(t)
ecCtx := context.Background()
discoveredForwarderAddr, err :=
contracts.CheckSwapCreatorContractCode(ecCtx, ec, ethcommon.HexToAddress(swapCreatorAddr))
require.NoError(t, err)
require.Equal(t, strings.ToLower(discoveredForwarderAddr.Hex()), forwarderAddr)
// something is seriously wrong if this next check fails, as CheckSwapCreatorContractCode
// should have already validated the forwarder bytecode
err = contracts.CheckForwarderContractCode(ecCtx, ec, ethcommon.HexToAddress(forwarderAddr))
err = contracts.CheckSwapCreatorContractCode(ecCtx, ec, ethcommon.HexToAddress(swapCreatorAddr))
require.NoError(t, err)
}
@@ -121,7 +111,7 @@ func TestDaemon_DevXMRMaker(t *testing.T) {
ec, _ := tests.NewEthClient(t)
// We tested --deploy with the taker, so test passing the contract address here
swapCreatorAddr, _, err := deploySwapCreator(context.Background(), ec, key, ethcommon.Address{}, t.TempDir())
swapCreatorAddr, err := deploySwapCreator(context.Background(), ec, key, t.TempDir())
require.NoError(t, err)
flags := []string{
@@ -158,9 +148,7 @@ func TestDaemon_BadFlags(t *testing.T) {
ec, _ := tests.NewEthClient(t)
ctx, _ := newTestContext(t)
swapCreatorAddr, swapCreator, err := deploySwapCreator(ctx, ec, key, ethcommon.Address{}, t.TempDir())
require.NoError(t, err)
forwarderAddr, err := swapCreator.TrustedForwarder(nil)
swapCreatorAddr, err := deploySwapCreator(ctx, ec, key, t.TempDir())
require.NoError(t, err)
baseFlags := []string{
@@ -184,35 +172,10 @@ func TestDaemon_BadFlags(t *testing.T) {
extraFlags: nil,
expectErr: `flag "deploy" or "contract-address" is required for env=dev`,
},
{
description: "deploy SwapCreator with invalid forwarder",
extraFlags: []string{
fmt.Sprintf("--%s", flagDeploy),
fmt.Sprintf("--%s=%s", flagForwarderAddress, swapCreatorAddr), // passing wrong contract
},
expectErr: "does not contain correct Forwarder code",
},
{
description: "pass invalid forwarder address (wrong length)",
extraFlags: []string{
fmt.Sprintf("--%s", flagDeploy),
fmt.Sprintf("--%s=%sAB", flagForwarderAddress, forwarderAddr), // one byte too long
},
expectErr: fmt.Sprintf(`"%s" requires a valid ethereum address`, flagForwarderAddress),
},
{
description: "pass forwarder address without deploy flag",
extraFlags: []string{
fmt.Sprintf("--%s=%s", flagForwarderAddress, forwarderAddr),
// next flag is needed, or we fail on a different error first
fmt.Sprintf("--%s=%s", flagContractAddress, swapCreatorAddr),
},
expectErr: fmt.Sprintf(`using flag "%s" requires the "%s" flag`, flagForwarderAddress, flagDeploy),
},
{
description: "pass invalid SwapCreator contract",
extraFlags: []string{
fmt.Sprintf("--%s=%s", flagContractAddress, forwarderAddr), // passing wrong contract
fmt.Sprintf("--%s=%s", flagContractAddress, ethcommon.Address{9}), // passing wrong contract
},
expectErr: "does not contain correct SwapCreator code",
},

View File

@@ -69,6 +69,7 @@ func MainnetConfig() *Config {
// Note: SwapCreator contract below is using GSN Forwarder address
// 0xB2b5841DBeF766d4b521221732F9B618fCf34A87
// https://docs.opengsn.org/networks/addresses.html
// TODO: this needs to be redeployed before this PR is merged
SwapCreatorAddr: ethcommon.HexToAddress("0xD3d19539D61bB0e7617E499C7262594E71CA1c66"),
Bootnodes: []string{
"/ip4/67.205.131.11/tcp/9909/p2p/12D3KooWGpCLC4y42rf6aR3cguVFJAruzFXT6mUEyp7C32jTsyJd",
@@ -102,7 +103,7 @@ func StagenetConfig() *Config {
Port: 38081,
},
},
SwapCreatorAddr: ethcommon.HexToAddress("0xEd014568991A9BE34F381Bf46d9c3f7623D4DEa5"),
SwapCreatorAddr: ethcommon.HexToAddress("0x90119FA88abE871B3e26DF3a57C29A450f006065"),
Bootnodes: []string{
"/ip4/134.122.115.208/tcp/9900/p2p/12D3KooWDqCzbjexHEa8Rut7bzxHFpRMZyDRW1L6TGkL1KY24JH5",
"/ip4/143.198.123.27/tcp/9900/p2p/12D3KooWSc4yFkPWBFmPToTMbhChH3FAgGH96DNzSg5fio1pQYoN",

View File

@@ -23,6 +23,7 @@ import (
"github.com/athanorlabs/atomic-swap/cliutil"
"github.com/athanorlabs/atomic-swap/coins"
"github.com/athanorlabs/atomic-swap/common/types"
contracts "github.com/athanorlabs/atomic-swap/ethereum"
"github.com/athanorlabs/atomic-swap/ethereum/block"
"github.com/athanorlabs/atomic-swap/ethereum/extethclient"
"github.com/athanorlabs/atomic-swap/monero"
@@ -78,19 +79,11 @@ func transfer(t *testing.T, fromKey *ecdsa.PrivateKey, toAddress ethcommon.Addre
func minimumFundAlice(t *testing.T, ec extethclient.EthClient, providesAmt *apd.Decimal) {
fundingKey := tests.GetTakerTestKey(t)
// When this comment was written, sample gas costs were:
// newSwap: 53787
// setReady: 34452
// refund: 46692
// relayClaim: 130507
//
const (
aliceGasRation = 150000 // roughly 10% more than newSwap+setRead+refund
aliceGasRation = contracts.MaxNewSwapETHGas + contracts.MaxSetReadyGas + contracts.MaxRefundETHGas
)
// We give Alice enough gas money to refund if needed, but not enough to
// relay a claim:
// 150000 - (53787 + 34452) = 61761
//
// relay a claim
suggestedGasPrice, err := ec.Raw().SuggestGasPrice(context.Background())
require.NoError(t, err)
gasCostWei := new(big.Int).Mul(suggestedGasPrice, big.NewInt(aliceGasRation))

View File

@@ -148,10 +148,7 @@ func getSwapCreatorAddress(t *testing.T, ec *ethclient.Client) ethcommon.Address
ctx := context.Background()
ethKey := tests.GetTakerTestKey(t) // requester might not have ETH, so we don't pass the key in
forwarderAddr, err := contracts.DeployGSNForwarderWithKey(ctx, ec, ethKey)
require.NoError(t, err)
swapCreatorAddr, _, err := contracts.DeploySwapCreatorWithKey(ctx, ec, ethKey, forwarderAddr)
swapCreatorAddr, _, err := contracts.DeploySwapCreatorWithKey(ctx, ec, ethKey)
require.NoError(t, err)
_swapCreatorAddr = &swapCreatorAddr

File diff suppressed because one or more lines are too long

View File

@@ -4,12 +4,10 @@
package contracts
import (
"bytes"
"context"
"errors"
"testing"
"github.com/athanorlabs/go-relayer/impls/gsnforwarder"
ethcommon "github.com/ethereum/go-ethereum/common"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
@@ -20,73 +18,33 @@ import (
// getContractCode is a test helper that deploys the swap creator contract to read back
// and return the finalised byte code post deployment.
func getContractCode(t *testing.T, forwarderAddr ethcommon.Address) []byte {
func getContractCode(t *testing.T) []byte {
ec, _ := tests.NewEthClient(t)
pk := tests.GetMakerTestKey(t)
contractAddr, _ := deploySwapCreatorWithForwarder(t, ec, pk, forwarderAddr)
contractAddr, _ := deploySwapCreator(t, ec, pk)
code, err := ec.CodeAt(context.Background(), contractAddr, nil)
require.NoError(t, err)
return code
}
func TestCheckForwarderContractCode(t *testing.T) {
ec, _ := tests.NewEthClient(t)
pk := tests.GetMakerTestKey(t)
forwarderAddr := deployForwarder(t, ec, pk)
err := CheckForwarderContractCode(context.Background(), ec, forwarderAddr)
require.NoError(t, err)
}
// This test will fail if the compiled SwapCreator contract is updated, but the
// expectedSwapCreatorBytecodeHex constant is not updated. Use this test to update the
// constant.
func TestExpectedSwapCreatorBytecodeHex(t *testing.T) {
allZeroTrustedForwarder := ethcommon.Address{}
codeHex := ethcommon.Bytes2Hex(getContractCode(t, allZeroTrustedForwarder))
codeHex := ethcommon.Bytes2Hex(getContractCode(t))
require.Equal(t, expectedSwapCreatorBytecodeHex, codeHex,
"update the expectedSwapCreatorBytecodeHex constant with the actual value to fix this test")
}
// This test will fail if the compiled SwapCreator contract is updated, but the
// forwarderAddrIndexes slice of trusted forwarder locations is not updated. Use
// this test to update the slice.
func TestForwarderAddrIndexes(t *testing.T) {
ec, _ := tests.NewEthClient(t)
pk := tests.GetMakerTestKey(t)
forwarderAddr := deployForwarder(t, ec, pk)
contactBytes := getContractCode(t, forwarderAddr)
addressLocations := make([]int, 0) // at the current time, there should always be 2
for i := 0; i < len(contactBytes)-ethAddrByteLen; i++ {
if bytes.Equal(contactBytes[i:i+ethAddrByteLen], forwarderAddr[:]) {
addressLocations = append(addressLocations, i)
i += ethAddrByteLen - 1 // -1 since the loop will increment by 1
}
}
t.Logf("forwarderAddrIndexes: %v", addressLocations)
require.EqualValues(t, forwarderAddrIndices, addressLocations,
"update forwarderAddrIndexes with above logged indexes to fix this test")
}
// Ensure that we correctly verify the SwapCreator contract when initialised with
// different trusted forwarder addresses.
func TestCheckSwapCreatorContractCode(t *testing.T) {
ec, _ := tests.NewEthClient(t)
pk := tests.GetMakerTestKey(t)
forwarderAddrs := []string{
deployForwarder(t, ec, pk).Hex(),
deployForwarder(t, ec, pk).Hex(),
deployForwarder(t, ec, pk).Hex(),
}
for _, addrHex := range forwarderAddrs {
tfAddr := ethcommon.HexToAddress(addrHex)
contractAddr, _ := deploySwapCreatorWithForwarder(t, ec, pk, tfAddr)
parsedTFAddr, err := CheckSwapCreatorContractCode(context.Background(), ec, contractAddr)
require.NoError(t, err)
require.Equal(t, addrHex, parsedTFAddr.Hex())
}
contractAddr, _ := deploySwapCreator(t, ec, pk)
err := CheckSwapCreatorContractCode(context.Background(), ec, contractAddr)
require.NoError(t, err)
}
// Tests that we fail when the wrong contract byte code is found
@@ -94,38 +52,37 @@ func TestCheckSwapCreatorContractCode_fail(t *testing.T) {
ec, _ := tests.NewEthClient(t)
pk := tests.GetMakerTestKey(t)
// Deploy a forwarder contract and then try to verify it as SwapCreator contract
contractAddr := deployForwarder(t, ec, pk)
_, err := CheckSwapCreatorContractCode(context.Background(), ec, contractAddr)
// Deploy a token contract and then try to verify it as SwapCreator contract
contractAddr, _ := deployERC20Token(t, ec, pk, "name", "symbol", 10, 100)
err := CheckSwapCreatorContractCode(context.Background(), ec, contractAddr)
require.ErrorIs(t, err, errInvalidSwapCreatorContract)
}
func TestSepoliaContract(t *testing.T) {
t.Skip("needs to be redeployed before merge")
ctx := context.Background()
ec := tests.NewEthSepoliaClient(t)
// temporarily place a funded sepolia private key below to deploy the test contract
const sepoliaKey = ""
parsedTFAddr, err := CheckSwapCreatorContractCode(ctx, ec, common.StagenetConfig().SwapCreatorAddr)
err := CheckSwapCreatorContractCode(ctx, ec, common.StagenetConfig().SwapCreatorAddr)
if errors.Is(err, errInvalidSwapCreatorContract) && sepoliaKey != "" {
pk, err := ethcrypto.HexToECDSA(sepoliaKey) //nolint:govet // shadow declaration of err
require.NoError(t, err)
forwarderAddr := ethcommon.HexToAddress(gsnforwarder.SepoliaForwarderAddrHex)
swapCreatorAddr, _, err := DeploySwapCreatorWithKey(ctx, ec, pk, forwarderAddr)
swapCreatorAddr, _, err := DeploySwapCreatorWithKey(ctx, ec, pk)
require.NoError(t, err)
t.Fatalf("Update common.StagenetConfig()'s SwapCreatorAddr with %s", swapCreatorAddr.Hex())
}
require.NoError(t, err)
require.Equal(t, gsnforwarder.SepoliaForwarderAddrHex, parsedTFAddr.Hex())
}
func TestMainnetContract(t *testing.T) {
t.Skip("needs to be redeployed before merge")
ctx := context.Background()
ec := tests.NewEthMainnetClient(t)
mainnetConf := common.MainnetConfig()
parsedTFAddr, err := CheckSwapCreatorContractCode(ctx, ec, mainnetConf.SwapCreatorAddr)
err := CheckSwapCreatorContractCode(ctx, ec, mainnetConf.SwapCreatorAddr)
require.NoError(t, err)
require.Equal(t, gsnforwarder.MainnetForwarderAddrHex, parsedTFAddr.Hex())
}

View File

@@ -4,12 +4,12 @@ package contracts
// ever see in a test, so you would need to adjust upwards a little to use as a
// gas limit. We use these values to estimate minimum required balances.
const (
MaxNewSwapETHGas = 50589
MaxNewSwapETHGas = 50639
MaxNewSwapTokenGas = 86218
MaxSetReadyGas = 31872
MaxSetReadyGas = 32054
MaxClaimETHGas = 43349
MaxClaimTokenGas = 47522
MaxRefundETHGas = 43120
MaxRefundTokenGas = 47282
MaxRefundETHGas = 43132
MaxRefundTokenGas = 47294
MaxTokenApproveGas = 47000 // 46223 with our contract
)

View File

@@ -3,6 +3,6 @@ package contracts
// We don't deploy SwapCreator contracts or ERC20 token contracts in swaps, so
// these constants are only compiled in for test files.
const (
maxSwapCreatorDeployGas = 1005177
maxTestERC20DeployGas = 798226 // using long token names or symbols will increase this
maxSwapCreatorDeployGas = 1094089
maxTestERC20DeployGas = 798286 // using long token names or symbols will increase this
)

View File

@@ -1,36 +0,0 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (metatx/ERC2771Context.sol)
pragma solidity ^0.8.19;
import {Context} from "./Context.sol";
/**
* @dev Context variant with ERC2771 support.
*/
abstract contract ERC2771Context is Context {
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
// TODO: this was modified to be public (is that ok?)
address public immutable _trustedForwarder;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor(address trustedForwarder) {
_trustedForwarder = trustedForwarder;
}
function isTrustedForwarder(address forwarder) public view virtual returns (bool) {
return forwarder == _trustedForwarder;
}
function _msgSender() internal view virtual override returns (address sender) {
if (isTrustedForwarder(msg.sender)) {
// The assembly code is more direct than the Solidity version using `abi.decode`.
/// @solidity memory-safe-assembly
assembly {
sender := shr(96, calldataload(sub(calldatasize(), 20)))
}
} else {
return super._msgSender();
}
}
}

View File

@@ -1,11 +1,10 @@
// SPDX-License-Identifier: LGPLv3
pragma solidity ^0.8.19;
import {ERC2771Context} from "./ERC2771Context.sol";
import {IERC20} from "./IERC20.sol";
import {Secp256k1} from "./Secp256k1.sol";
contract SwapCreator is ERC2771Context, Secp256k1 {
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
@@ -41,6 +40,20 @@ contract SwapCreator is ERC2771Context, Secp256k1 {
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.
struct RelaySwap {
// the swap the transaction is for
Swap swap;
// the fee, in wei, paid to the relayer
uint256 fee;
// hash of (relayer's payout address || 4-byte salt)
bytes32 relayerHash;
// address of the swap contract this transaction is meant for
address swapCreator;
}
mapping(bytes32 => Stage) public swaps;
event New(
@@ -80,9 +93,6 @@ contract SwapCreator is ERC2771Context, Secp256k1 {
// returned when the caller of `setReady` or `refund` is not the swap owner
error OnlySwapOwner();
// returned when `claimRelayer` is not called by the trusted forwarder
error OnlyTrustedForwarder();
// returned when the signer of the relayed transaction is not the swap's claimer
error OnlySwapClaimer();
@@ -104,7 +114,15 @@ contract SwapCreator is ERC2771Context, Secp256k1 {
// returned when the provided secret does not match the expected public key
error InvalidSecret();
constructor(address trustedForwarder) ERC2771Context(trustedForwarder) {} // solhint-disable-line
// returned 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
error InvalidContractAddress();
// returned 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.
@@ -174,8 +192,9 @@ contract SwapCreator is ERC2771Context, Secp256k1 {
// Bob can claim if:
// - (Alice has set the swap to `ready` or it's past timeout0) and it's before timeout1
function claim(Swap memory _swap, bytes32 _s) public {
_claim(_swap, _s);
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)) {
@@ -190,45 +209,59 @@ contract SwapCreator is ERC2771Context, Secp256k1 {
// Bob can claim if:
// - (Alice has set the swap to `ready` or it's past timeout0) and it's before timeout1
// This function is only callable by the trusted forwarder.
// It transfers the fee to the originator of the transaction.
function claimRelayer(Swap memory _swap, bytes32 _s, uint256 fee) public {
if (!isTrustedForwarder(msg.sender)) revert OnlyTrustedForwarder();
_claim(_swap, _s);
// It transfers the fee to the relayer address specified in `_relaySwap`.
// Note: this function will revert if the swap value is less than the relayer fee;
// in that case, `claim` must be called instead.
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
// which is sent to the originator of the transaction.
// tx.origin is okay here, since it isn't for authentication purposes.
if (_swap.asset == address(0)) {
_swap.claimer.transfer(_swap.value - fee);
payable(tx.origin).transfer(fee); // solhint-disable-line
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(_swap.asset).transfer(_swap.claimer, _swap.value - fee);
IERC20(_swap.asset).transfer(tx.origin, fee); // solhint-disable-line
IERC20(_relaySwap.swap.asset).transfer(
_relaySwap.swap.claimer,
_relaySwap.swap.value - _relaySwap.fee
);
IERC20(_relaySwap.swap.asset).transfer(_relayer, _relaySwap.fee);
}
}
function _claim(Swap memory _swap, bytes32 _s) internal {
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 (_msgSender() != _swap.claimer) revert OnlySwapClaimer();
if (block.timestamp < _swap.timeout0 && swapStage != Stage.READY) revert TooEarlyToClaim();
if (block.timestamp >= _swap.timeout1) revert TooLateToClaim();
verifySecret(_s, _swap.pubKeyClaim);
emit Claimed(swapID, _s);
verifySecret(_secret, _swap.pubKeyClaim);
emit Claimed(swapID, _secret);
swaps[swapID] = Stage.COMPLETED;
}
// Alice can claim a refund:
// - Until timeout0 unless she calls setReady
// - After timeout1
function refund(Swap memory _swap, bytes32 _s) public {
function refund(Swap memory _swap, bytes32 _secret) public {
bytes32 swapID = keccak256(abi.encode(_swap));
Stage swapStage = swaps[swapID];
if (swapStage == Stage.INVALID) revert InvalidSwap();
@@ -239,8 +272,8 @@ contract SwapCreator is ERC2771Context, Secp256k1 {
(block.timestamp > _swap.timeout0 || swapStage == Stage.READY)
) revert NotTimeToRefund();
verifySecret(_s, _swap.pubKeyRefund);
emit Refunded(swapID, _s);
verifySecret(_secret, _swap.pubKeyRefund);
emit Refunded(swapID, _secret);
// send asset back to swap owner
swaps[swapID] = Stage.COMPLETED;
@@ -251,7 +284,7 @@ contract SwapCreator is ERC2771Context, Secp256k1 {
}
}
function verifySecret(bytes32 _s, bytes32 _hashedPubkey) internal pure {
if (!mulVerify(uint256(_s), uint256(_hashedPubkey))) revert InvalidSecret();
function verifySecret(bytes32 _secret, bytes32 _hashedPubkey) internal pure {
if (!mulVerify(uint256(_secret), uint256(_hashedPubkey))) revert InvalidSecret();
}
}

View File

@@ -8,8 +8,6 @@ import (
"crypto/ecdsa"
"fmt"
"github.com/athanorlabs/go-relayer/common"
"github.com/athanorlabs/go-relayer/impls/gsnforwarder"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
@@ -21,26 +19,19 @@ import (
var log = logging.Logger("contracts")
// DeploySwapCreatorWithKey deploys the SwapCreator contract using the passed privKey to
// pay for the gas.
// pay for the deployment.
func DeploySwapCreatorWithKey(
ctx context.Context,
ec *ethclient.Client,
privKey *ecdsa.PrivateKey,
forwarderAddr ethcommon.Address,
) (ethcommon.Address, *SwapCreator, error) {
txOpts, err := newTXOpts(ctx, ec, privKey)
if err != nil {
return ethcommon.Address{}, nil, err
}
if (forwarderAddr != ethcommon.Address{}) {
if err = registerDomainSeparatorIfNeeded(ctx, ec, privKey, forwarderAddr); err != nil {
return ethcommon.Address{}, nil, fmt.Errorf("failed to deploy swap creator: %w", err)
}
}
log.Infof("deploying SwapCreator.sol with forwarderAddr %s", forwarderAddr)
address, tx, sf, err := DeploySwapCreator(txOpts, ec, forwarderAddr)
log.Infof("deploying SwapCreator.sol")
address, tx, sf, err := DeploySwapCreator(txOpts, ec)
if err != nil {
return ethcommon.Address{}, nil, fmt.Errorf("failed to deploy swap creator: %w", err)
}
@@ -54,109 +45,6 @@ func DeploySwapCreatorWithKey(
return address, sf, nil
}
// DeployGSNForwarderWithKey deploys and registers the GSN forwarder using the passed
// private key to pay the gas fees.
func DeployGSNForwarderWithKey(
ctx context.Context,
ec *ethclient.Client,
privKey *ecdsa.PrivateKey,
) (ethcommon.Address, error) {
txOpts, err := newTXOpts(ctx, ec, privKey)
if err != nil {
return ethcommon.Address{}, err
}
address, tx, contract, err := gsnforwarder.DeployForwarder(txOpts, ec)
if err != nil {
return ethcommon.Address{}, fmt.Errorf("failed to deploy Forwarder.sol: %w", err)
}
_, err = block.WaitForReceipt(ctx, ec, tx.Hash())
if err != nil {
return ethcommon.Address{}, err
}
err = registerDomainSeparator(ctx, ec, privKey, address, contract)
if err != nil {
return ethcommon.Address{}, err
}
return address, nil
}
func isDomainSeparatorRegistered(
ctx context.Context,
ec *ethclient.Client,
forwarderAddr ethcommon.Address,
forwarder *gsnforwarder.Forwarder,
) (isRegistered bool, err error) {
chainID, err := ec.ChainID(ctx)
if err != nil {
return false, err
}
name := gsnforwarder.DefaultName
version := gsnforwarder.DefaultVersion
ds, err := common.GetEIP712DomainSeparator(name, version, chainID, forwarderAddr)
if err != nil {
return false, err
}
opts := &bind.CallOpts{Context: ctx}
return forwarder.Domains(opts, ds)
}
func registerDomainSeparatorIfNeeded(
ctx context.Context,
ec *ethclient.Client,
privKey *ecdsa.PrivateKey,
forwarderAddr ethcommon.Address,
) error {
forwarder, err := gsnforwarder.NewForwarder(forwarderAddr, ec)
if err != nil {
return err
}
isRegistered, err := isDomainSeparatorRegistered(ctx, ec, forwarderAddr, forwarder)
if err != nil {
return err
}
if isRegistered {
return nil
}
return registerDomainSeparator(ctx, ec, privKey, forwarderAddr, forwarder)
}
func registerDomainSeparator(
ctx context.Context,
ec *ethclient.Client,
privKey *ecdsa.PrivateKey,
forwarderAddr ethcommon.Address,
forwarder *gsnforwarder.Forwarder,
) error {
log.Infof("registering domain separator for forwarder %s", forwarderAddr)
txOpts, err := newTXOpts(ctx, ec, privKey)
if err != nil {
return err
}
tx, err := forwarder.RegisterDomainSeparator(txOpts, gsnforwarder.DefaultName, gsnforwarder.DefaultVersion)
if err != nil {
return fmt.Errorf("failed to register domain separator: %w", err)
}
_, err = block.WaitForReceipt(ctx, ec, tx.Hash())
if err != nil {
return err
}
log.Debugf("registered domain separator in forwarder at %s: name=%s version=%s",
forwarderAddr,
gsnforwarder.DefaultName,
gsnforwarder.DefaultVersion,
)
return nil
}
func newTXOpts(ctx context.Context, ec *ethclient.Client, privkey *ecdsa.PrivateKey) (*bind.TransactOpts, error) {
chainID, err := ec.ChainID(ctx)
if err != nil {

View File

@@ -1,38 +0,0 @@
// Copyright 2023 The AthanorLabs/atomic-swap Authors
// SPDX-License-Identifier: LGPL-3.0-only
package contracts
import (
"context"
"testing"
"github.com/athanorlabs/go-relayer/impls/gsnforwarder"
"github.com/stretchr/testify/require"
"github.com/athanorlabs/atomic-swap/tests"
)
func Test_registerDomainSeparatorIfNeeded(t *testing.T) {
ec, _ := tests.NewEthClient(t)
ctx := context.Background()
privKey := tests.GetMakerTestKey(t)
txOpts, err := newTXOpts(ctx, ec, privKey)
require.NoError(t, err)
forwarderAddr, tx, forwarder, err := gsnforwarder.DeployForwarder(txOpts, ec)
require.NoError(t, err)
_ = tests.MineTransaction(t, ec, tx)
isRegistered, err := isDomainSeparatorRegistered(ctx, ec, forwarderAddr, forwarder)
require.NoError(t, err)
require.False(t, isRegistered)
err = registerDomainSeparatorIfNeeded(ctx, ec, privKey, forwarderAddr)
require.NoError(t, err)
isRegistered, err = isDomainSeparatorRegistered(ctx, ec, forwarderAddr, forwarder)
require.NoError(t, err)
require.True(t, isRegistered)
}

View File

@@ -4,81 +4,113 @@
package contracts
import (
"crypto/ecdsa"
"math/big"
"testing"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require"
"github.com/athanorlabs/atomic-swap/common/types"
"github.com/athanorlabs/atomic-swap/tests"
)
func TestSwapCreator_NewSwap_ERC20(t *testing.T) {
pkA := tests.GetTakerTestKey(t)
ec, _ := tests.NewEthClient(t)
addr := crypto.PubkeyToAddress(pkA.PublicKey)
func deployERC20Token(
t *testing.T,
ec *ethclient.Client,
pk *ecdsa.PrivateKey, // token owner (and pays for deployment)
name string,
symbol string,
decimals uint8,
supplyStdUnits int64,
) (ethcommon.Address, *TestERC20) {
addr := crypto.PubkeyToAddress(pk.PublicKey)
supply := new(big.Int).Mul(big.NewInt(supplyStdUnits),
new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimals)), nil),
)
// deploy TestERC20
erc20Addr, tx, erc20Contract, err :=
DeployTestERC20(getAuth(t, pkA), ec, "Test of the ERC20 Token", "ERC20Token", 18, addr, big.NewInt(9999))
tokenAddr, tx, tokenContract, err :=
DeployTestERC20(getAuth(t, pk), ec, name, symbol, decimals, addr, supply)
require.NoError(t, err)
receipt := getReceipt(t, ec, tx)
t.Logf("gas cost to deploy TestERC20.sol: %d (delta %d)",
receipt.GasUsed, maxTestERC20DeployGas-int(receipt.GasUsed))
require.GreaterOrEqual(t, maxTestERC20DeployGas, int(receipt.GasUsed))
testNewSwap(t, types.EthAsset(erc20Addr), erc20Contract)
return tokenAddr, tokenContract
}
func TestSwapCreator_NewSwap_ERC20(t *testing.T) {
pkA := tests.GetTakerTestKey(t)
ec, _ := tests.NewEthClient(t)
tokenAddr, tokenContract := deployERC20Token(
t,
ec,
pkA,
"Test of the ERC20 Token",
"ERC20Token",
18,
9999,
)
testNewSwap(t, types.EthAsset(tokenAddr), tokenContract)
}
func TestSwapCreator_Claim_ERC20(t *testing.T) {
pkA := tests.GetTakerTestKey(t)
ec, _ := tests.NewEthClient(t)
addr := crypto.PubkeyToAddress(pkA.PublicKey)
erc20Addr, tx, erc20Contract, err :=
DeployTestERC20(getAuth(t, pkA), ec, "TestERC20", "TEST", 18, addr, big.NewInt(9999))
require.NoError(t, err)
receipt := getReceipt(t, ec, tx)
t.Logf("gas cost to deploy TestERC20.sol: %d (delta %d)",
receipt.GasUsed, maxTestERC20DeployGas-int(receipt.GasUsed))
require.GreaterOrEqual(t, maxTestERC20DeployGas, int(receipt.GasUsed))
tokenAddr, tokenContract := deployERC20Token(
t,
ec,
pkA,
"TestERC20",
"TEST",
18,
9999,
)
// 3 logs:
// Approval
// Transfer
// New
testClaim(t, types.EthAsset(erc20Addr), 2, big.NewInt(99), erc20Contract)
testClaim(t, types.EthAsset(tokenAddr), 2, big.NewInt(99), tokenContract)
}
func TestSwapCreator_RefundBeforeT0_ERC20(t *testing.T) {
pkA := tests.GetTakerTestKey(t)
ec, _ := tests.NewEthClient(t)
addr := crypto.PubkeyToAddress(pkA.PublicKey)
erc20Addr, tx, erc20Contract, err :=
DeployTestERC20(getAuth(t, pkA), ec, "TestERC20", "TEST", 18, addr, big.NewInt(9999))
require.NoError(t, err)
receipt := getReceipt(t, ec, tx)
t.Logf("gas cost to deploy TestERC20.sol: %d (delta %d)",
receipt.GasUsed, maxTestERC20DeployGas-int(receipt.GasUsed))
require.GreaterOrEqual(t, maxTestERC20DeployGas, int(receipt.GasUsed))
tokenAddr, tokenContract := deployERC20Token(
t,
ec,
pkA,
"TestERC20",
"TEST",
18,
9999,
)
testRefundBeforeT0(t, types.EthAsset(erc20Addr), erc20Contract, 2)
testRefundBeforeT0(t, types.EthAsset(tokenAddr), tokenContract, 2)
}
func TestSwapCreator_RefundAfterT1_ERC20(t *testing.T) {
pkA := tests.GetTakerTestKey(t)
ec, _ := tests.NewEthClient(t)
addr := crypto.PubkeyToAddress(pkA.PublicKey)
erc20Addr, tx, erc20Contract, err :=
DeployTestERC20(getAuth(t, pkA), ec, "TestERC20", "TestERC20", 18, addr, big.NewInt(9999))
require.NoError(t, err)
receipt := getReceipt(t, ec, tx)
t.Logf("gas cost to deploy TestERC20.sol: %d (delta %d)",
receipt.GasUsed, maxTestERC20DeployGas-int(receipt.GasUsed))
require.GreaterOrEqual(t, maxTestERC20DeployGas, int(receipt.GasUsed))
tokenAddr, tokenContract := deployERC20Token(
t,
ec,
pkA,
"TestERC20",
"TEST",
18,
9999,
)
testRefundAfterT1(t, types.EthAsset(erc20Addr), erc20Contract, 2)
testRefundAfterT1(t, types.EthAsset(tokenAddr), tokenContract, 2)
}

File diff suppressed because one or more lines are too long

View File

@@ -63,19 +63,8 @@ func approveERC20(t *testing.T,
require.GreaterOrEqual(t, MaxTokenApproveGas, int(receipt.GasUsed), "Token Approve")
}
func deployForwarder(t *testing.T, ec *ethclient.Client, pk *ecdsa.PrivateKey) ethcommon.Address {
forwarderAddr, err := DeployGSNForwarderWithKey(context.Background(), ec, pk)
require.NoError(t, err)
return forwarderAddr
}
func deploySwapCreatorWithForwarder(
t *testing.T,
ec *ethclient.Client,
pk *ecdsa.PrivateKey,
forwarderAddr ethcommon.Address,
) (ethcommon.Address, *SwapCreator) {
swapCreatorAddr, tx, swapCreator, err := DeploySwapCreator(getAuth(t, pk), ec, forwarderAddr)
func deploySwapCreator(t *testing.T, ec *ethclient.Client, pk *ecdsa.PrivateKey) (ethcommon.Address, *SwapCreator) {
swapCreatorAddr, tx, swapCreator, err := DeploySwapCreator(getAuth(t, pk), ec)
require.NoError(t, err)
receipt := getReceipt(t, ec, tx)
@@ -86,11 +75,6 @@ func deploySwapCreatorWithForwarder(
return swapCreatorAddr, swapCreator
}
func deploySwapCreator(t *testing.T, ec *ethclient.Client, pk *ecdsa.PrivateKey) (ethcommon.Address, *SwapCreator) {
forwarderAddr := deployForwarder(t, ec, pk)
return deploySwapCreatorWithForwarder(t, ec, pk, forwarderAddr)
}
func testNewSwap(t *testing.T, asset types.EthAsset, erc20Contract *TestERC20) {
pk := tests.GetTakerTestKey(t)
ec, _ := tests.NewEthClient(t)

View File

@@ -56,6 +56,86 @@ func StageToString(stage byte) string {
}
}
// Hash abi-encodes the RelaySwap and returns the keccak256 hash of the encoded value.
func (s *SwapCreatorRelaySwap) Hash() types.Hash {
uint256Ty, err := abi.NewType("uint256", "", nil)
if err != nil {
panic(fmt.Sprintf("failed to create uint256 type: %s", err))
}
bytes32Ty, err := abi.NewType("bytes32", "", nil)
if err != nil {
panic(fmt.Sprintf("failed to create bytes32 type: %s", err))
}
addressTy, err := abi.NewType("address", "", nil)
if err != nil {
panic(fmt.Sprintf("failed to create address type: %s", err))
}
arguments := abi.Arguments{
{
Type: addressTy,
},
{
Type: addressTy,
},
{
Type: bytes32Ty,
},
{
Type: bytes32Ty,
},
{
Type: uint256Ty,
},
{
Type: uint256Ty,
},
{
Type: addressTy,
},
{
Type: uint256Ty,
},
{
Type: uint256Ty,
},
{
Type: uint256Ty,
},
{
Type: bytes32Ty,
},
{
Type: addressTy,
},
}
args, err := arguments.Pack(
s.Swap.Owner,
s.Swap.Claimer,
s.Swap.PubKeyClaim,
s.Swap.PubKeyRefund,
s.Swap.Timeout0,
s.Swap.Timeout1,
s.Swap.Asset,
s.Swap.Value,
s.Swap.Nonce,
s.Fee,
s.RelayerHash,
s.SwapCreator,
)
if err != nil {
// As long as none of the *big.Int fields are nil, this cannot fail.
// When receiving SwapCreatorRelaySwap objects from peers in
// JSON, all *big.Int values are pre-validated to be non-nil.
panic(fmt.Sprintf("failed to pack arguments: %s", err))
}
return crypto.Keccak256Hash(args)
}
// SwapID calculates and returns the same hashed swap identifier that newSwap
// emits and that is used to track the on-chain stage of a swap.
func (sfs *SwapCreatorSwap) SwapID() types.Hash {
@@ -131,17 +211,6 @@ func GetSecretFromLog(log *ethtypes.Log, eventTopic [32]byte) (*mcrypto.PrivateS
return nil, errors.New("invalid event, must be one of Claimed or Refunded")
}
// abiSF, err := abi.JSON(strings.NewReader(SwapCreatorMetaData.ABI))
// if err != nil {
// return nil, err
// }
// data := log.Data
// res, err := abiSF.Unpack(event, data)
// if err != nil {
// return nil, err
// }
if len(log.Topics) < 3 {
return nil, errors.New("log had not enough parameters")
}
@@ -165,17 +234,6 @@ func CheckIfLogIDMatches(log ethtypes.Log, eventTopic, id [32]byte) (bool, error
return false, errors.New("invalid event, must be one of Claimed or Refunded")
}
// abi, err := abi.JSON(strings.NewReader(SwapCreatorMetaData.ABI))
// if err != nil {
// return false, err
// }
// data := log.Data
// res, err := abi.Unpack(event, data)
// if err != nil {
// return false, err
// }
if len(log.Topics) < 2 {
return false, errors.New("log had not enough parameters")
}

1
go.mod
View File

@@ -9,7 +9,6 @@ require (
github.com/Masterminds/semver/v3 v3.2.1
github.com/athanorlabs/go-dleq v0.1.0
github.com/athanorlabs/go-p2p-net v0.2.0
github.com/athanorlabs/go-relayer v0.2.0
github.com/btcsuite/btcd/btcutil v1.1.3
github.com/cockroachdb/apd/v3 v3.1.2
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0

2
go.sum
View File

@@ -41,8 +41,6 @@ github.com/athanorlabs/go-dleq v0.1.0 h1:0/llWZG8fz2uintMBKOiBC502zCsDA8nt8vxI73
github.com/athanorlabs/go-dleq v0.1.0/go.mod h1:DWry6jSD7A13MKmeZA0AX3/xBeQCXDoygX99VPwL3yU=
github.com/athanorlabs/go-p2p-net v0.2.0 h1:+VpAN10Ys0B28QDXQRaDySvNfHS99Jt83Qq1sUhEnG4=
github.com/athanorlabs/go-p2p-net v0.2.0/go.mod h1:egbDohZq6I4FzKaVytR+xZKUwA2OqTE6mr9dsNQPPbE=
github.com/athanorlabs/go-relayer v0.2.0 h1:cYwEadgLWotWBlCx+uhLZphQna3EKEekHhNViHgaSSo=
github.com/athanorlabs/go-relayer v0.2.0/go.mod h1:xh6P9KTXNS9zENAT3QyOBP3sdyHtefrtBLugKeI8oJ4=
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=

View File

@@ -143,6 +143,7 @@ func (h *Host) SetHandlers(makerHandler MakerHandler, relayHandler RelayHandler)
h.h.SetStreamHandler(queryProtocolID, h.handleQueryStream)
h.h.SetStreamHandler(relayProtocolID, h.handleRelayStream)
h.h.SetStreamHandler(relayerQueryProtocolID, h.handleRelayerQueryStream)
h.h.SetStreamHandler(swapID, h.handleProtocolStream)
}

View File

@@ -32,7 +32,7 @@ type mockMakerHandler struct {
id types.Hash
}
func (h *mockMakerHandler) GetOffers() []*types.Offer {
func (*mockMakerHandler) GetOffers() []*types.Offer {
return []*types.Offer{}
}
@@ -50,7 +50,15 @@ type mockRelayHandler struct {
t *testing.T
}
func (h *mockRelayHandler) HandleRelayClaimRequest(_ peer.ID, _ *RelayClaimRequest) (*RelayClaimResponse, error) {
func (*mockRelayHandler) GetRelayerAddressHash() (types.Hash, error) {
return types.Hash{99}, nil
}
func (*mockRelayHandler) HasOngoingSwapAsTaker(_ peer.ID) error {
return nil
}
func (*mockRelayHandler) HandleRelayClaimRequest(_ peer.ID, _ *RelayClaimRequest) (*RelayClaimResponse, error) {
return &RelayClaimResponse{
TxHash: mockEthTXHash,
}, nil
@@ -68,11 +76,11 @@ func (s *mockSwapState) OfferID() types.Hash {
return testID
}
func (s *mockSwapState) HandleProtocolMessage(_ Message) error {
func (*mockSwapState) HandleProtocolMessage(_ Message) error {
return nil
}
func (s *mockSwapState) Exit() error {
func (*mockSwapState) Exit() error {
return nil
}

View File

@@ -24,6 +24,7 @@ import (
const (
Unknown byte = iota // occupies the uninitialized value
QueryResponseType
RelayerQueryResponseType
RelayClaimRequestType
RelayClaimResponseType
SendKeysType
@@ -39,6 +40,8 @@ func TypeToString(t byte) string {
return "SendKeysMessage"
case NotifyETHLockedType:
return "NotifyETHLocked"
case RelayerQueryResponseType:
return "RelayerQueryResponseType"
case RelayClaimRequestType:
return "RelayClaimRequestType"
case RelayClaimResponseType:
@@ -62,6 +65,8 @@ func DecodeMessage(b []byte) (common.Message, error) {
switch msgType {
case QueryResponseType:
msg = new(QueryResponse)
case RelayerQueryResponseType:
msg = new(RelayerQueryResponse)
case RelayClaimRequestType:
msg = new(RelayClaimRequest)
case RelayClaimResponseType:

View File

@@ -13,21 +13,42 @@ import (
contracts "github.com/athanorlabs/atomic-swap/ethereum"
)
// RelayerQueryResponse is sent from a relayer to the opener of
// a /relayerquery/0 stream.
type RelayerQueryResponse struct {
AddressHash []byte `json:"address" validate:"required,len=32"`
}
// String converts the RelayerQueryResponse to a string usable for debugging purposes
func (m *RelayerQueryResponse) String() string {
return fmt.Sprintf("RelayerQueryResponse=%#v", m)
}
// Encode implements the Encode() method of the common.Message interface which
// prepends a message type byte before the message's JSON encoding.
func (m *RelayerQueryResponse) Encode() ([]byte, error) {
b, err := vjson.MarshalStruct(m)
if err != nil {
return nil, err
}
return append([]byte{RelayerQueryResponseType}, b...), nil
}
// Type implements the Type() method of the common.Message interface
func (m *RelayerQueryResponse) Type() byte {
return RelayerQueryResponseType
}
// RelayClaimRequest implements common.Message for our p2p relay claim requests.
type RelayClaimRequest struct {
// OfferID is non-nil, if the request is from a maker to the taker of an
// active swap. It is nil, if the request is being sent to a relay node,
// because it advertised in the DHT.
OfferID *types.Hash `json:"offerID"`
SwapCreatorAddr ethcommon.Address `json:"swapCreatorAddr" validate:"required"`
Swap *contracts.SwapCreatorSwap `json:"swap" validate:"required"`
Secret []byte `json:"secret" validate:"required,len=32"`
Signature []byte `json:"signature" validate:"required,len=65"`
}
// RelayClaimResponse implements common.Message for our p2p relay claim responses
type RelayClaimResponse struct {
TxHash ethcommon.Hash `json:"transactionHash" validate:"required"`
OfferID *types.Hash `json:"offerID"`
RelaySwap *contracts.SwapCreatorRelaySwap `json:"relaySwap" validate:"required"`
Secret []byte `json:"secret" validate:"required,len=32"`
Signature []byte `json:"signature" validate:"required,len=65"`
}
// String converts the RelayClaimRequest to a string usable for debugging purposes
@@ -51,6 +72,11 @@ func (m *RelayClaimRequest) Type() byte {
return RelayClaimRequestType
}
// RelayClaimResponse implements common.Message for our p2p relay claim responses
type RelayClaimResponse struct {
TxHash ethcommon.Hash `json:"transactionHash" validate:"required"`
}
// String converts the RelayClaimRequest to a string usable for debugging purposes
func (m *RelayClaimResponse) String() string {
return fmt.Sprintf("RelayClaimResponse=%#v", m)

View File

@@ -13,12 +13,15 @@ import (
libp2pnetwork "github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/athanorlabs/atomic-swap/common/types"
"github.com/athanorlabs/atomic-swap/net/message"
)
const (
relayProtocolID = "/relay/0"
relayerQueryProtocolID = "/relayerquery/0"
// RelayerProvidesStr is the DHT namespace advertised by nodes willing to relay
// claims for arbitrary XMR makers.
RelayerProvidesStr = "relayer"
@@ -31,9 +34,87 @@ func (h *Host) DiscoverRelayers() ([]peer.ID, error) {
return h.Discover(RelayerProvidesStr, defaultDiscoverTime)
}
// we need the relayer to send a message containing
// the address to send the fee to, so that the requester
// can sign it.
func (h *Host) handleRelayerQueryStream(stream libp2pnetwork.Stream) {
defer func() { _ = stream.Close() }()
if !h.isRelayer {
err := h.relayHandler.HasOngoingSwapAsTaker(stream.Conn().RemotePeer())
if err != nil {
// the returned error logs the peer ID
log.Debugf("ignoring relayer query: %s", err)
return
}
}
addressHash, err := h.relayHandler.GetRelayerAddressHash()
if err != nil {
log.Warnf("failed to get relayer address hash: %s", err)
return
}
addrResp := &message.RelayerQueryResponse{
AddressHash: addressHash[:],
}
log.Debugf("sending RelayerQueryResponse to peer %s", stream.Conn().RemotePeer())
if err := p2pnet.WriteStreamMessage(stream, addrResp, stream.Conn().RemotePeer()); err != nil {
log.Warnf("failed to send RelayClaimResponse message to peer: %s", err)
}
}
// QueryRelayerAddress opens a relay stream with a peer, and if they are a relayer,
// they will respond with their relayer payout address.
func (h *Host) QueryRelayerAddress(relayerID peer.ID) (types.Hash, error) {
ctx, cancel := context.WithTimeout(h.ctx, connectionTimeout)
defer cancel()
if err := h.h.Connect(ctx, peer.AddrInfo{ID: relayerID}); err != nil {
return types.Hash{}, err
}
stream, err := h.h.NewStream(ctx, relayerID, relayerQueryProtocolID)
if err != nil {
return types.Hash{}, fmt.Errorf("failed to open stream with peer: err=%w", err)
}
log.Debugf("opened relayer query stream: %s", stream.Conn())
resp, err := receiveRelayerQueryResponse(stream)
if err != nil {
return types.Hash{}, err
}
return resp, nil
}
func receiveRelayerQueryResponse(stream libp2pnetwork.Stream) (types.Hash, error) {
const relayResponseTimeout = time.Second * 15
select {
case msg := <-nextStreamMessage(stream, maxRelayMessageSize):
if msg == nil {
return types.Hash{}, errors.New("failed to read RelayerQueryResponse")
}
resp, ok := msg.(*message.RelayerQueryResponse)
if !ok {
return types.Hash{}, fmt.Errorf("expected %s message but received %s",
message.TypeToString(message.RelayClaimResponseType),
message.TypeToString(msg.Type()))
}
return [32]byte(resp.AddressHash), nil
case <-time.After(relayResponseTimeout):
return types.Hash{}, errors.New("timed out waiting for QueryResponse")
}
}
func (h *Host) handleRelayStream(stream libp2pnetwork.Stream) {
defer func() { _ = stream.Close() }()
// TODO: add timeout for receiving request
msg, err := readStreamMessage(stream, maxRelayMessageSize)
if err != nil {
log.Debugf("error reading RelayClaimRequest: %s", err)
@@ -72,15 +153,15 @@ func (h *Host) handleRelayStream(stream libp2pnetwork.Stream) {
return
}
log.Debugf("Relayed claim for %s with tx=%s", req.Swap.Claimer, resp.TxHash)
log.Debugf("Relayed claim for %s with tx=%s", req.RelaySwap.Swap.Claimer, resp.TxHash)
if err := p2pnet.WriteStreamMessage(stream, resp, stream.Conn().RemotePeer()); err != nil {
log.Warnf("failed to send RelayClaimResponse message to peer: %s", err)
return
}
}
// SubmitClaimToRelayer sends a request to relay a swap claim to a peer.
func (h *Host) SubmitClaimToRelayer(relayerID peer.ID, request *RelayClaimRequest) (*RelayClaimResponse, error) {
// SubmitRelayRequest sends a request to relay a swap claim to a peer.
func (h *Host) SubmitRelayRequest(relayerID peer.ID, request *RelayClaimRequest) (*RelayClaimResponse, error) {
ctx, cancel := context.WithTimeout(h.ctx, connectionTimeout)
defer cancel()
@@ -92,9 +173,8 @@ func (h *Host) SubmitClaimToRelayer(relayerID peer.ID, request *RelayClaimReques
if err != nil {
return nil, fmt.Errorf("failed to open stream with peer: err=%w", err)
}
defer func() { _ = stream.Close() }()
log.Debugf("opened relay stream: %s", stream.Conn())
log.Debugf("opened relay stream with peer %s", relayerID)
if err := p2pnet.WriteStreamMessage(stream, request, relayerID); err != nil {
log.Warnf("failed to send RelayClaimRequest to peer: err=%s", err)

View File

@@ -58,18 +58,24 @@ func createTestClaimRequest() *message.RelayClaimRequest {
secret := [32]byte{0x1}
sig := [65]byte{0x1}
swap := contracts.SwapCreatorSwap{
Owner: ethcommon.Address{0x1},
Claimer: ethcommon.Address{0x1},
PubKeyClaim: [32]byte{0x1},
PubKeyRefund: [32]byte{0x1},
Timeout0: big.NewInt(time.Now().Add(30 * time.Minute).Unix()),
Timeout1: 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{
SwapCreatorAddr: ethcommon.Address{0x1},
Swap: &contracts.SwapCreatorSwap{
Owner: ethcommon.Address{0x1},
Claimer: ethcommon.Address{0x1},
PubKeyClaim: [32]byte{0x1},
PubKeyRefund: [32]byte{0x1},
Timeout0: big.NewInt(time.Now().Add(30 * time.Minute).Unix()),
Timeout1: big.NewInt(time.Now().Add(60 * time.Minute).Unix()),
Asset: ethcommon.Address(types.EthAssetETH),
Value: big.NewInt(1e18),
Nonce: big.NewInt(1),
RelaySwap: &contracts.SwapCreatorRelaySwap{
Swap: swap,
Fee: big.NewInt(9e15),
RelayerHash: [32]byte{1},
SwapCreator: ethcommon.Address{0x3},
},
Secret: secret[:],
Signature: sig[:],
@@ -81,8 +87,11 @@ func createTestClaimRequest() *message.RelayClaimRequest {
func TestHost_SubmitClaimToRelayer_dhtRelayer(t *testing.T) {
ha, hb := twoHostRelayerSetup(t)
_, err := ha.QueryRelayerAddress(hb.PeerID())
require.NoError(t, err)
// success path ha->hb, hb is a DHT relayer
resp, err := ha.SubmitClaimToRelayer(hb.PeerID(), createTestClaimRequest())
resp, err := ha.SubmitRelayRequest(hb.PeerID(), createTestClaimRequest())
require.NoError(t, err)
require.Equal(t, mockEthTXHash.Hex(), resp.TxHash.Hex())
@@ -90,7 +99,7 @@ func TestHost_SubmitClaimToRelayer_dhtRelayer(t *testing.T) {
// does not pass back the exact reason for rejecting a claim to avoid
// possible privacy data leaks, but in this case it is because hb is not
// a DHT advertising relayer.
_, err = hb.SubmitClaimToRelayer(ha.PeerID(), createTestClaimRequest())
_, err = hb.SubmitRelayRequest(ha.PeerID(), createTestClaimRequest())
require.ErrorContains(t, err, "failed to read RelayClaimResponse")
}
@@ -102,7 +111,7 @@ func TestHost_SubmitClaimToRelayer_xmrTakerRelayer(t *testing.T) {
request.OfferID = &offerID
// should ignore offerID and succeed
response, err := hb.SubmitClaimToRelayer(ha.PeerID(), request)
response, err := hb.SubmitRelayRequest(ha.PeerID(), request)
require.NoError(t, err)
require.Equal(t, mockEthTXHash, response.TxHash)
}
@@ -112,11 +121,11 @@ func TestHost_SubmitClaimToRelayer_fail(t *testing.T) {
req := createTestClaimRequest()
req.Secret = []byte{0x1} // wrong size
_, err := ha.SubmitClaimToRelayer(hb.PeerID(), req)
_, err := ha.SubmitRelayRequest(hb.PeerID(), req)
require.ErrorContains(t, err, "Field validation for 'Secret' failed on the 'len' tag")
req = createTestClaimRequest()
req.Signature = []byte{0x1, 0x2} // wrong size
_, err = ha.SubmitClaimToRelayer(hb.PeerID(), req)
_, err = ha.SubmitRelayRequest(hb.PeerID(), req)
require.ErrorContains(t, err, "Field validation for 'Signature' failed on the 'len' tag")
}

View File

@@ -4,13 +4,12 @@
package net
import (
libp2pnetwork "github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/athanorlabs/atomic-swap/common"
"github.com/athanorlabs/atomic-swap/common/types"
"github.com/athanorlabs/atomic-swap/net/message"
libp2pnetwork "github.com/libp2p/go-libp2p/core/network"
)
type SwapState = common.SwapStateNet //nolint:revive
@@ -35,7 +34,9 @@ type MakerHandler interface {
// RelayHandler handles relay claim requests. It is implemented by
// *backend.backend.
type RelayHandler interface {
GetRelayerAddressHash() (types.Hash, error)
HandleRelayClaimRequest(remotePeer peer.ID, msg *RelayClaimRequest) (*RelayClaimResponse, error)
HasOngoingSwapAsTaker(remotePeer peer.ID) error
}
type swap struct {

View File

@@ -7,12 +7,14 @@ package backend
import (
"context"
"crypto/rand"
"errors"
"fmt"
"sync"
"time"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/athanorlabs/atomic-swap/common"
@@ -32,8 +34,9 @@ import (
type NetSender interface {
SendSwapMessage(common.Message, types.Hash) error
CloseProtocolStream(id types.Hash)
DiscoverRelayers() ([]peer.ID, error) // Only used by Maker
SubmitClaimToRelayer(peer.ID, *message.RelayClaimRequest) (*message.RelayClaimResponse, error) // Only used by Taker
DiscoverRelayers() ([]peer.ID, error) // Only used by Maker
QueryRelayerAddress(peer.ID) (types.Hash, error) // only used by taker
SubmitRelayRequest(peer.ID, *message.RelayClaimRequest) (*message.RelayClaimResponse, error) // only used by taker
}
// RecoveryDB is implemented by *db.RecoveryDB
@@ -66,6 +69,14 @@ type Backend interface {
// helpers
NewSwapCreator(addr ethcommon.Address) (*contracts.SwapCreator, error)
HandleRelayClaimRequest(remotePeer peer.ID, request *message.RelayClaimRequest) (*message.RelayClaimResponse, error)
GetRelayerAddressHash() (types.Hash, error)
HasOngoingSwapAsTaker(peer.ID) error
SubmitClaimToRelayer(
peer.ID,
*types.Hash,
*contracts.SwapCreatorRelaySwap,
[32]byte,
) (*message.RelayClaimResponse, error) // Only used by Taker
// getters
Ctx() context.Context
@@ -107,6 +118,10 @@ type backend struct {
// network interface
NetSender
// map of hash(relayer address || salt) -> salt
relayerHashMu sync.RWMutex
relayerHash map[types.Hash][4]byte
}
// Config is the config for the Backend
@@ -144,6 +159,7 @@ func NewBackend(cfg *Config) (Backend, error) {
NetSender: cfg.Net,
perSwapXMRDepositAddr: make(map[types.Hash]*mcrypto.Address),
recoveryDB: cfg.RecoveryDB,
relayerHash: make(map[types.Hash][4]byte),
}, nil
}
@@ -236,11 +252,34 @@ func (b *backend) ClearXMRDepositAddress(offerID types.Hash) {
delete(b.perSwapXMRDepositAddr, offerID)
}
// HasOngoingSwapAsTaker returns nil if we have an ongoing swap with the given peer where
// we're the xmrtaker, otherwise returns an error.
func (b *backend) HasOngoingSwapAsTaker(remotePeer peer.ID) error {
swaps, err := b.swapManager.GetOngoingSwaps()
if err != nil {
return err
}
for _, swap := range swaps {
if swap.PeerID != remotePeer {
continue
}
if swap.IsTaker() {
return nil
}
}
return fmt.Errorf("do not have an ongoing swap with peer %s as taker", remotePeer)
}
// HandleRelayClaimRequest validates and sends the transaction for a relay claim request
func (b *backend) HandleRelayClaimRequest(
remotePeer peer.ID,
request *message.RelayClaimRequest,
) (*message.RelayClaimResponse, error) {
defer b.clearRelayerAddressHash(request.RelaySwap.RelayerHash)
if request.OfferID != nil {
has := b.swapManager.HasOngoingSwap(*request.OfferID)
if !has {
@@ -273,15 +312,68 @@ func (b *backend) HandleRelayClaimRequest(
if err != nil {
return nil, fmt.Errorf("swap info for taker claim request not found: %w", err)
}
if swapInfo.SwapID != request.Swap.SwapID() {
if swapInfo.SwapID != request.RelaySwap.Swap.SwapID() {
return nil, errors.New("counterparty claim request has invalid swap ID")
}
}
b.relayerHashMu.RLock()
salt := b.relayerHash[request.RelaySwap.RelayerHash]
b.relayerHashMu.RUnlock()
return relayer.ValidateAndSendTransaction(
b.Ctx(),
request,
b.ETHClient(),
b.SwapCreatorAddr(),
salt,
)
}
func (b *backend) GetRelayerAddressHash() (types.Hash, error) {
address := b.ETHClient().Address()
var salt [4]byte
_, err := rand.Read(salt[:])
if err != nil {
return types.Hash{}, err
}
hash := crypto.Keccak256Hash(append(address.Bytes(), salt[:]...))
b.relayerHashMu.Lock()
defer b.relayerHashMu.Unlock()
b.relayerHash[hash] = salt
return hash, nil
}
func (b *backend) clearRelayerAddressHash(hash types.Hash) {
b.relayerHashMu.Lock()
defer b.relayerHashMu.Unlock()
delete(b.relayerHash, hash)
}
func (b *backend) SubmitClaimToRelayer(
relayerID peer.ID,
offerID *types.Hash,
relaySwap *contracts.SwapCreatorRelaySwap,
secret [32]byte,
) (*message.RelayClaimResponse, error) {
// get the relayer's address hash
relayerAddrHash, err := b.QueryRelayerAddress(relayerID)
if err != nil {
return nil, err
}
// set relayer address hash and sign as front-run prevention
relaySwap.RelayerHash = relayerAddrHash
req, err := relayer.CreateRelayClaimRequest(b.ETHClient().PrivateKey(), relaySwap, secret)
if err != nil {
return nil, err
}
if offerID != nil {
req.OfferID = offerID
}
return b.SubmitRelayRequest(relayerID, req)
}

View File

@@ -11,7 +11,6 @@ import (
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
@@ -19,10 +18,9 @@ import (
"github.com/athanorlabs/atomic-swap/coins"
"github.com/athanorlabs/atomic-swap/common"
"github.com/athanorlabs/atomic-swap/common/types"
contracts "github.com/athanorlabs/atomic-swap/ethereum"
"github.com/athanorlabs/atomic-swap/ethereum/block"
"github.com/athanorlabs/atomic-swap/ethereum/extethclient"
"github.com/athanorlabs/atomic-swap/net/message"
"github.com/athanorlabs/atomic-swap/relayer"
)
// claimFunds redeems XMRMaker's ETH funds by calling Claim() on the contract
@@ -124,12 +122,17 @@ func checkForMinClaimBalance(ctx context.Context, ec extethclient.EthClient) (bo
// relayClaimWithXMRTaker relays the claim to the swap's XMR taker, who should
// process the claim even if they are not relaying claims for everyone.
func (s *swapState) relayClaimWithXMRTaker(request *message.RelayClaimRequest) (*ethtypes.Receipt, error) {
// only requests to the XMR taker set the offerID field
request.OfferID = &s.offer.ID
defer func() { request.OfferID = nil }()
func (s *swapState) relayClaimWithXMRTaker() (*ethtypes.Receipt, error) {
secret := s.getSecret()
relaySwap := &contracts.SwapCreatorRelaySwap{
Swap: *s.contractSwap,
SwapCreator: s.swapCreatorAddr,
Fee: coins.RelayerFeeWei,
// this is set when we receive the relayer's address hash
RelayerHash: types.Hash{},
}
response, err := s.Backend.SubmitClaimToRelayer(s.info.PeerID, request)
response, err := s.Backend.SubmitClaimToRelayer(s.info.PeerID, &s.offer.ID, relaySwap, secret)
if err != nil {
return nil, err
}
@@ -153,7 +156,16 @@ func (s *swapState) relayClaimWithXMRTaker(request *message.RelayClaimRequest) (
// claimWithAdvertisedRelayers relays the claim to nodes that advertise
// themselves as relayers in the DHT until the claim succeeds, all relayers have
// been tried, or the context is cancelled.
func (s *swapState) claimWithAdvertisedRelayers(request *message.RelayClaimRequest) (*ethtypes.Receipt, error) {
func (s *swapState) claimWithAdvertisedRelayers() (*ethtypes.Receipt, error) {
secret := s.getSecret()
relaySwap := &contracts.SwapCreatorRelaySwap{
Swap: *s.contractSwap,
SwapCreator: s.swapCreatorAddr,
Fee: coins.RelayerFeeWei,
// this is set when we receive the relayer's address hash
RelayerHash: types.Hash{},
}
relayers, err := s.Backend.DiscoverRelayers()
if err != nil {
return nil, err
@@ -170,7 +182,7 @@ func (s *swapState) claimWithAdvertisedRelayers(request *message.RelayClaimReque
}
log.Debugf("submitting claim to relayer with peer ID %s", relayerPeerID)
resp, err := s.Backend.SubmitClaimToRelayer(relayerPeerID, request)
resp, err := s.Backend.SubmitClaimToRelayer(relayerPeerID, nil, relaySwap, secret)
if err != nil {
log.Warnf("failed to submit tx to relayer: %s", err)
continue
@@ -204,31 +216,11 @@ func (s *swapState) claimWithAdvertisedRelayers(request *message.RelayClaimReque
// operations more generally. Note that the receipt returned is for a
// transaction created by the remote relayer, not by us.
func (s *swapState) claimWithRelay() (*ethtypes.Receipt, error) {
forwarderAddr, err := s.SwapCreator().TrustedForwarder(&bind.CallOpts{Context: s.ctx})
if err != nil {
return nil, err
}
secret := s.getSecret()
request, err := relayer.CreateRelayClaimRequest(
s.ctx,
s.ETHClient().PrivateKey(),
s.ETHClient().Raw(),
s.swapCreatorAddr,
forwarderAddr,
s.contractSwap,
&secret,
)
if err != nil {
return nil, err
}
receipt, err := s.claimWithAdvertisedRelayers(request)
receipt, err := s.claimWithAdvertisedRelayers()
if err != nil {
log.Warnf("failed to relay with DHT-advertised relayers: %s", err)
log.Infof("falling back to swap counterparty as relayer")
return s.relayClaimWithXMRTaker(request)
return s.relayClaimWithXMRTaker()
}
return receipt, nil
}

View File

@@ -31,7 +31,6 @@ import (
"github.com/athanorlabs/atomic-swap/tests"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)
@@ -62,12 +61,16 @@ func (n *mockNet) DiscoverRelayers() ([]peer.ID, error) {
return nil, nil
}
func (n *mockNet) SubmitClaimToRelayer(_ peer.ID, _ *message.RelayClaimRequest) (*message.RelayClaimResponse, error) {
func (n *mockNet) SubmitRelayRequest(_ peer.ID, _ *message.RelayClaimRequest) (*message.RelayClaimResponse, error) {
return new(message.RelayClaimResponse), nil
}
func (n *mockNet) CloseProtocolStream(_ types.Hash) {}
func (n *mockNet) QueryRelayerAddress(_ peer.ID) (types.Hash, error) {
return types.Hash{}, nil
}
func newSwapManager(t *testing.T) pswap.Manager {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
@@ -88,8 +91,7 @@ func newBackendAndNet(t *testing.T) (backend.Backend, *mockNet) {
txOpts, err := bind.NewKeyedTransactorWithChainID(pk, chainID)
require.NoError(t, err)
var forwarderAddr ethcommon.Address
_, tx, _, err := contracts.DeploySwapCreator(txOpts, ec, forwarderAddr)
_, tx, _, err := contracts.DeploySwapCreator(txOpts, ec)
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())

View File

@@ -107,10 +107,7 @@ func (s *swapState) handleNotifyETHLocked(msg *message.NotifyETHLocked) error {
}
contractAddr := msg.Address
// note: this function verifies the forwarder code as well, even if we aren't using a relayer,
// in which case it's not relevant to us and we don't need to verify it.
// doesn't hurt though I suppose.
_, err = contracts.CheckSwapCreatorContractCode(s.ctx, s.Backend.ETHClient().Raw(), contractAddr)
err = contracts.CheckSwapCreatorContractCode(s.ctx, s.Backend.ETHClient().Raw(), contractAddr)
if err != nil {
return err
}

View File

@@ -46,11 +46,7 @@ func validateMinBalance(
// offer of XMR for ETH
func validateMinBalForETHSwap(weiBalance *coins.WeiAmount, providesAmt *apd.Decimal, gasPriceWei *big.Int) error {
providedAmtWei := coins.EtherToWei(providesAmt).BigInt()
neededGas := big.NewInt(
contracts.MaxNewSwapETHGas +
contracts.MaxSetReadyGas +
contracts.MaxRefundTokenGas,
)
neededGas := big.NewInt(contracts.MaxNewSwapETHGas + contracts.MaxSetReadyGas + contracts.MaxRefundETHGas)
neededWeiForGas := new(big.Int).Mul(neededGas, gasPriceWei)
neededBalanceWei := new(big.Int).Add(providedAmtWei, neededWeiForGas)

View File

@@ -387,20 +387,24 @@ func (s *swapState) exit() error {
// we should also refund in this case, since we might be past t1.
receipt, err := s.tryRefund()
if err != nil {
if errors.Is(err, errRefundSwapCompleted) {
s.clearNextExpectedEvent(types.CompletedRefund)
log.Infof("swap was already refunded")
return nil
}
if errors.Is(err, errRefundSwapCompleted) || strings.Contains(err.Error(), revertSwapCompleted) {
log.Infof("swap was already completed")
if strings.Contains(err.Error(), revertSwapCompleted) {
// note: this should NOT ever error; it could if the ethclient
// or monero clients crash during the course of the claim,
// but that would be very bad.
err = s.tryClaim()
if err != nil {
if errors.Is(err, errNoClaimLogsFound) {
// in this case, assume we refunded
s.clearNextExpectedEvent(types.CompletedRefund)
return nil
}
// note: this should NOT occur; it could if the ethclient
// or monero clients crash during the course of the claim,
// but that would be very bad.
return fmt.Errorf("failed to claim even though swap was completed on-chain: %w", err)
}
return nil
}
return fmt.Errorf("failed to refund: %w", err)

View File

@@ -65,12 +65,16 @@ func (n *mockNet) DiscoverRelayers() ([]peer.ID, error) {
return nil, nil
}
func (n *mockNet) SubmitClaimToRelayer(_ peer.ID, _ *message.RelayClaimRequest) (*message.RelayClaimResponse, error) {
func (n *mockNet) SubmitRelayRequest(_ peer.ID, _ *message.RelayClaimRequest) (*message.RelayClaimResponse, error) {
return new(message.RelayClaimResponse), nil
}
func (n *mockNet) CloseProtocolStream(_ types.Hash) {}
func (*mockNet) QueryRelayerAddress(_ peer.ID) (types.Hash, error) {
return types.Hash{99}, nil
}
func newSwapManager(t *testing.T) pswap.Manager {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
@@ -94,8 +98,7 @@ func newBackendAndNet(t *testing.T) (backend.Backend, *mockNet) {
txOpts, err := bind.NewKeyedTransactorWithChainID(pk, ec.ChainID())
require.NoError(t, err)
var forwarderAddr ethcommon.Address
_, tx, _, err := contracts.DeploySwapCreator(txOpts, ec.Raw(), forwarderAddr)
_, tx, _, err := contracts.DeploySwapCreator(txOpts, ec.Raw())
require.NoError(t, err)
addr, err := bind.WaitDeployed(ctx, ec.Raw(), tx)

View File

@@ -5,54 +5,67 @@
package relayer
import (
"context"
"crypto/ecdsa"
"fmt"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
logging "github.com/ipfs/go-log"
contracts "github.com/athanorlabs/atomic-swap/ethereum"
"github.com/athanorlabs/atomic-swap/net/message"
)
const (
relayedClaimGas = 70000 // worst case gas usage for the claimRelayer swapFactory call
forwarderClaimGas = 156000 // worst case gas usage when using forwarder to claim
)
var log = logging.Logger("relayer")
// CreateRelayClaimRequest fills and returns a RelayClaimRequest ready for
// submission to a relayer.
func CreateRelayClaimRequest(
ctx context.Context,
claimerEthKey *ecdsa.PrivateKey,
ec *ethclient.Client,
swapCreatorAddr ethcommon.Address,
forwarderAddr ethcommon.Address,
swap *contracts.SwapCreatorSwap,
secret *[32]byte,
relaySwap *contracts.SwapCreatorRelaySwap,
secret [32]byte,
) (*message.RelayClaimRequest, error) {
signature, err := createForwarderSignature(
ctx,
signature, err := createRelayClaimSignature(
claimerEthKey,
ec,
swapCreatorAddr,
forwarderAddr,
swap,
secret,
relaySwap,
)
if err != nil {
return nil, err
}
return &message.RelayClaimRequest{
OfferID: nil, // set elsewhere if sending to counterparty
SwapCreatorAddr: swapCreatorAddr,
Swap: swap,
Secret: secret[:],
Signature: signature,
OfferID: nil, // set elsewhere if sending to counterparty
RelaySwap: relaySwap,
Secret: secret[:],
Signature: signature,
}, nil
}
func createRelayClaimSignature(
claimerEthKey *ecdsa.PrivateKey,
relaySwap *contracts.SwapCreatorRelaySwap,
) ([]byte, error) {
signerAddress := ethcrypto.PubkeyToAddress(claimerEthKey.PublicKey)
if relaySwap.Swap.Claimer != signerAddress {
return nil, fmt.Errorf("signing key %s does not match claimer %s", signerAddress, relaySwap.Swap.Claimer)
}
// signature format is (r || s || v), v = 27/28
signature, err := Sign(claimerEthKey, relaySwap.Hash())
if err != nil {
return nil, fmt.Errorf("failed to sign relay request: %w", err)
}
return signature, nil
}
// Sign signs the given digest and returns a 65-byte signature in (r,s,v) format.
func Sign(key *ecdsa.PrivateKey, digest [32]byte) ([]byte, error) {
sig, err := ethcrypto.Sign(digest[:], key)
if err != nil {
return nil, err
}
// Ethereum wants 27/28 for v
sig[64] += 27
return sig, nil
}

View File

@@ -22,24 +22,19 @@ import (
// Speed up tests a little by giving deployContracts(...) a package-level cache.
// These variables should not be accessed by other functions.
var _forwarderAddr *ethcommon.Address
var _swapCreatorAddr *ethcommon.Address
// deployContracts deploys and returns the swapCreator and forwarder addresses.
func deployContracts(t *testing.T, ec *ethclient.Client, key *ecdsa.PrivateKey) (ethcommon.Address, ethcommon.Address) {
// deployContracts deploys and returns the swapCreator addresses.
func deployContracts(t *testing.T, ec *ethclient.Client, key *ecdsa.PrivateKey) ethcommon.Address {
ctx := context.Background()
if _forwarderAddr == nil || _swapCreatorAddr == nil {
forwarderAddr, err := contracts.DeployGSNForwarderWithKey(ctx, ec, key)
require.NoError(t, err)
_forwarderAddr = &forwarderAddr
swapCreatorAddr, _, err := contracts.DeploySwapCreatorWithKey(ctx, ec, key, forwarderAddr)
if _swapCreatorAddr == nil {
swapCreatorAddr, _, err := contracts.DeploySwapCreatorWithKey(ctx, ec, key)
require.NoError(t, err)
_swapCreatorAddr = &swapCreatorAddr
}
return *_swapCreatorAddr, *_forwarderAddr
return *_swapCreatorAddr
}
func createTestSwap(claimer ethcommon.Address) *contracts.SwapCreatorSwap {
@@ -57,21 +52,26 @@ func createTestSwap(claimer ethcommon.Address) *contracts.SwapCreatorSwap {
}
func TestCreateRelayClaimRequest(t *testing.T) {
ctx := context.Background()
ethKey := tests.GetMakerTestKey(t)
claimer := crypto.PubkeyToAddress(*ethKey.Public().(*ecdsa.PublicKey))
ec, _ := tests.NewEthClient(t)
secret := [32]byte{0x1}
swapCreatorAddr, forwarderAddr := deployContracts(t, ec, ethKey)
swapCreatorAddr := deployContracts(t, ec, ethKey)
// success path
swap := createTestSwap(claimer)
req, err := CreateRelayClaimRequest(ctx, ethKey, ec, swapCreatorAddr, forwarderAddr, swap, &secret)
relaySwap := &contracts.SwapCreatorRelaySwap{
Swap: *swap,
Fee: big.NewInt(1),
SwapCreator: swapCreatorAddr,
RelayerHash: types.Hash{},
}
req, err := CreateRelayClaimRequest(ethKey, relaySwap, secret)
require.NoError(t, err)
require.NotNil(t, req)
// change the ethkey to not match the claimer address to trigger the error path
ethKey = tests.GetTakerTestKey(t)
_, err = CreateRelayClaimRequest(ctx, ethKey, ec, swapCreatorAddr, forwarderAddr, swap, &secret)
require.ErrorContains(t, err, "signing key does not match claimer")
_, err = CreateRelayClaimRequest(ethKey, relaySwap, secret)
require.ErrorContains(t, err, "does not match claimer")
}

View File

@@ -1,124 +0,0 @@
// Copyright 2023 The AthanorLabs/atomic-swap Authors
// SPDX-License-Identifier: LGPL-3.0-only
package relayer
import (
"context"
"crypto/ecdsa"
"fmt"
"math/big"
rcommon "github.com/athanorlabs/go-relayer/common"
"github.com/athanorlabs/go-relayer/impls/gsnforwarder"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/athanorlabs/atomic-swap/coins"
contracts "github.com/athanorlabs/atomic-swap/ethereum"
)
func createForwarderSignature(
ctx context.Context,
claimerEthKey *ecdsa.PrivateKey,
ec *ethclient.Client,
swapCreatorAddr ethcommon.Address,
forwarderAddr ethcommon.Address,
swap *contracts.SwapCreatorSwap,
secret *[32]byte,
) ([]byte, error) {
if swap.Claimer != ethcrypto.PubkeyToAddress(claimerEthKey.PublicKey) {
return nil, fmt.Errorf("signing key does not match claimer %s", swap.Claimer)
}
forwarder, domainSeparator, err := getForwarderAndDomainSeparator(ctx, ec, forwarderAddr)
if err != nil {
return nil, err
}
nonce, err := forwarder.GetNonce(&bind.CallOpts{Context: ctx}, swap.Claimer)
if err != nil {
return nil, err
}
forwarderReq, err := createForwarderRequest(
nonce,
swapCreatorAddr,
swap,
secret,
)
if err != nil {
return nil, err
}
digest, err := rcommon.GetForwardRequestDigestToSign(forwarderReq, *domainSeparator, nil)
if err != nil {
return nil, fmt.Errorf("failed to get forward request digest: %w", err)
}
signature, err := rcommon.NewKeyFromPrivateKey(claimerEthKey).Sign(digest)
if err != nil {
return nil, fmt.Errorf("failed to sign forward request digest: %w", err)
}
return signature, nil
}
// createForwarderRequest creates the forwarder request, which we sign the digest of.
func createForwarderRequest(
nonce *big.Int,
swapCreatorAddr ethcommon.Address,
swap *contracts.SwapCreatorSwap,
secret *[32]byte,
) (*gsnforwarder.IForwarderForwardRequest, error) {
calldata, err := getClaimRelayerTxCalldata(coins.RelayerFeeWei, swap, secret)
if err != nil {
return nil, err
}
req := &gsnforwarder.IForwarderForwardRequest{
From: swap.Claimer,
To: swapCreatorAddr,
Value: big.NewInt(0),
Gas: big.NewInt(relayedClaimGas),
Nonce: nonce,
Data: calldata,
ValidUntilTime: big.NewInt(0),
}
return req, nil
}
// getClaimRelayerTxCalldata returns the call data to be used when invoking the
// claimRelayer method on the SwapCreator contract.
func getClaimRelayerTxCalldata(feeWei *big.Int, swap *contracts.SwapCreatorSwap, secret *[32]byte) ([]byte, error) {
return contracts.SwapCreatorParsedABI.Pack("claimRelayer", *swap, *secret, feeWei)
}
func getForwarderAndDomainSeparator(
ctx context.Context,
ec *ethclient.Client,
forwarderAddr ethcommon.Address,
) (*gsnforwarder.Forwarder, *[32]byte, error) {
chainID, err := ec.ChainID(ctx)
if err != nil {
return nil, nil, err
}
forwarder, err := gsnforwarder.NewForwarder(forwarderAddr, ec)
if err != nil {
return nil, nil, err
}
domainSeparator, err := rcommon.GetEIP712DomainSeparator(gsnforwarder.DefaultName,
gsnforwarder.DefaultVersion, chainID, forwarderAddr)
if err != nil {
return nil, nil, fmt.Errorf("failed to get EIP712 domain separator: %w", err)
}
return forwarder, &domainSeparator, nil
}

View File

@@ -5,11 +5,10 @@ package relayer
import (
"context"
"errors"
"encoding/binary"
"fmt"
"math/big"
"github.com/athanorlabs/go-relayer/impls/gsnforwarder"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
@@ -23,46 +22,31 @@ import (
"github.com/athanorlabs/atomic-swap/net/message"
)
const (
maxClaimRelayerETHGas = 100000 // worst case gas usage for the claimRelayer call (ether)
// actual cost is 85040 but that fails in unit tests on "out of gas".
)
// ValidateAndSendTransaction sends the relayed transaction to the network if it validates successfully.
func ValidateAndSendTransaction(
ctx context.Context,
req *message.RelayClaimRequest,
ec extethclient.EthClient,
ourSFContractAddr ethcommon.Address,
ourSwapCreatorAddr ethcommon.Address,
salt [4]byte,
) (*message.RelayClaimResponse, error) {
err := validateClaimRequest(ctx, req, ec.Raw(), ourSFContractAddr)
err := validateClaimRequest(ctx, req, ec.Raw(), ec.Address(), salt, ourSwapCreatorAddr)
if err != nil {
return nil, err
}
reqSwapCreator, err := contracts.NewSwapCreator(req.SwapCreatorAddr, ec.Raw())
if err != nil {
return nil, err
}
reqForwarderAddr, err := reqSwapCreator.TrustedForwarder(&bind.CallOpts{Context: ctx})
if err != nil {
return nil, err
}
reqForwarder, domainSeparator, err := getForwarderAndDomainSeparator(ctx, ec.Raw(), reqForwarderAddr)
if err != nil {
return nil, err
}
nonce, err := reqForwarder.GetNonce(&bind.CallOpts{Context: ctx}, req.Swap.Claimer)
reqSwapCreator, err := contracts.NewSwapCreator(req.RelaySwap.SwapCreator, ec.Raw())
if err != nil {
return nil, err
}
// The size of request.Secret was vetted when it was deserialized
secret := (*[32]byte)(req.Secret)
forwarderReq, err := createForwarderRequest(nonce, req.SwapCreatorAddr, req.Swap, secret)
if err != nil {
return nil, err
}
secret := [32]byte(req.Secret)
gasPrice, err := checkForMinClaimBalance(ctx, ec)
if err != nil {
@@ -78,32 +62,40 @@ func ValidateAndSendTransaction(
return nil, err
}
txOpts.GasPrice = gasPrice
txOpts.GasLimit = forwarderClaimGas
txOpts.GasLimit = maxClaimRelayerETHGas
log.Debugf("relaying tx with gas price %s and gas limit %d", gasPrice, txOpts.GasLimit)
err = simulateExecute(
v := req.Signature[64]
r := [32]byte(req.Signature[:32])
s := [32]byte(req.Signature[32:64])
saltU32 := binary.BigEndian.Uint32(salt[:])
err = simulateClaimRelayer(
ctx,
ec,
&reqForwarderAddr,
txOpts,
*forwarderReq,
*domainSeparator,
req.Signature,
req.RelaySwap,
secret,
ec.Address(),
saltU32,
v, r, s,
)
if err != nil {
return nil, err
}
tx, err := reqForwarder.Execute(
tx, err := reqSwapCreator.ClaimRelayer(
txOpts,
*forwarderReq,
*domainSeparator,
gsnforwarder.ForwardRequestTypehash,
nil,
req.Signature,
*req.RelaySwap,
secret,
ec.Address(),
saltU32,
v,
r,
s,
)
if err != nil {
log.Errorf("failed to call execute: %s", err)
log.Errorf("failed to call ClaimRelayer: %s", err)
return nil, err
}
@@ -113,7 +105,6 @@ func ValidateAndSendTransaction(
}
log.Infof("relayed claim %s", common.ReceiptInfo(receipt))
return &message.RelayClaimResponse{TxHash: tx.Hash()}, nil
}
@@ -130,7 +121,7 @@ func checkForMinClaimBalance(ctx context.Context, ec extethclient.EthClient) (*b
return nil, err
}
txCost := new(big.Int).Mul(gasPrice, big.NewInt(forwarderClaimGas))
txCost := new(big.Int).Mul(gasPrice, big.NewInt(maxClaimRelayerETHGas))
if balance.BigInt().Cmp(txCost) < 0 {
return nil, fmt.Errorf("balance %s ETH is under the minimum %s ETH to relay claim",
balance.AsEtherString(), coins.FmtWeiAsETH(txCost))
@@ -139,30 +130,30 @@ func checkForMinClaimBalance(ctx context.Context, ec extethclient.EthClient) (*b
return gasPrice, nil
}
// simulateExecute calls the forwarder's execute method (defined in Forwarder.sol)
// simulateExecute calls the swap creator's ClaimRelayer function
// with CallContract which executes the method call without mining it into the blockchain.
// https://pkg.go.dev/github.com/ethereum/go-ethereum/ethclient#Client.CallContract
func simulateExecute(
func simulateClaimRelayer(
ctx context.Context,
ec extethclient.EthClient,
reqForwarderAddr *ethcommon.Address,
txOpts *bind.TransactOpts,
forwarderReq gsnforwarder.IForwarderForwardRequest,
domainSeparator [32]byte,
sig []byte,
relaySwap *contracts.SwapCreatorRelaySwap,
secret [32]byte,
relayer ethcommon.Address,
salt uint32,
v uint8,
r, s [32]byte,
) error {
forwarderABI, err := gsnforwarder.ForwarderMetaData.GetAbi()
if err != nil {
return err
}
// Pack the "execute" method call
packed, err := forwarderABI.Pack(
"execute",
forwarderReq,
domainSeparator,
gsnforwarder.ForwardRequestTypehash,
[]byte{},
sig,
// Pack the "claimRelayer" method call
packed, err := contracts.SwapCreatorParsedABI.Pack(
"claimRelayer",
*relaySwap,
secret,
relayer,
salt,
v,
r,
s,
)
if err != nil {
return err
@@ -170,7 +161,7 @@ func simulateExecute(
callMessage := ethereum.CallMsg{
From: txOpts.From,
To: reqForwarderAddr,
To: &relaySwap.SwapCreator,
Gas: txOpts.GasLimit,
GasPrice: txOpts.GasPrice,
GasFeeCap: txOpts.GasFeeCap,
@@ -180,26 +171,12 @@ func simulateExecute(
AccessList: []types.AccessTuple{},
}
// Call the "execute" method
data, err := ec.Raw().CallContract(ctx, callMessage, nil)
// Call the "claimRelayer" method
// will return a revert error on failure
_, err = ec.Raw().CallContract(ctx, callMessage, nil)
if err != nil {
return err
}
// Unpack the response data
response := struct {
Success bool
Ret []byte
}{Success: false, Ret: []byte{}}
err = forwarderABI.UnpackIntoInterface(&response, "execute", data)
if err != nil {
return err
}
if !response.Success {
return errors.New("relayed transaction failed on simulation")
}
return nil
}

View File

@@ -6,6 +6,7 @@ package relayer
import (
"context"
"crypto/ecdsa"
"crypto/rand"
"math/big"
"testing"
@@ -21,7 +22,11 @@ import (
)
func Test_ValidateAndSendTransaction(t *testing.T) {
sk := tests.GetMakerTestKey(t)
sk := tests.GetMakerTestKey(t) // name of this is a bit misleading
relayerPub := sk.Public().(*ecdsa.PublicKey)
relayerAddr := crypto.PubkeyToAddress(*relayerPub)
t.Log("relayerAddr: ", relayerAddr)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -39,11 +44,14 @@ func Test_ValidateAndSendTransaction(t *testing.T) {
// hash public key of claim secret
cmt := res.Secp256k1PublicKey().Keccak256()
pub := sk.Public().(*ecdsa.PublicKey)
addr := crypto.PubkeyToAddress(*pub)
swapCreatorAddr, forwarderAddr := deployContracts(t, ec.Raw(), sk)
// generate claimer key; should be different from relayer key
claimerSk, err := crypto.GenerateKey()
require.NoError(t, err)
pub := claimerSk.Public().(*ecdsa.PublicKey)
claimerAddr := crypto.PubkeyToAddress(*pub)
t.Log("claimerAddr: ", claimerAddr)
swapCreatorAddr := deployContracts(t, ec.Raw(), sk)
swapCreator, err := contracts.NewSwapCreator(swapCreatorAddr, ec.Raw())
require.NoError(t, err)
@@ -55,8 +63,17 @@ func Test_ValidateAndSendTransaction(t *testing.T) {
txOpts.Value = value
refundKey := [32]byte{1}
tx, err := swapCreator.NewSwap(txOpts, cmt, refundKey, addr,
testT0Timeout, testT1Timeout, types.EthAssetETH.Address(), value, nonce)
tx, err := swapCreator.NewSwap(
txOpts,
cmt,
refundKey,
claimerAddr,
testT0Timeout,
testT1Timeout,
types.EthAssetETH.Address(),
value,
nonce,
)
require.NoError(t, err)
receipt, err := block.WaitForReceipt(ctx, ec.Raw(), tx.Hash())
require.NoError(t, err)
@@ -71,9 +88,9 @@ func Test_ValidateAndSendTransaction(t *testing.T) {
t0, t1, err := contracts.GetTimeoutsFromLog(receipt.Logs[logIndex])
require.NoError(t, err)
swap := &contracts.SwapCreatorSwap{
Owner: addr,
Claimer: addr,
swap := contracts.SwapCreatorSwap{
Owner: relayerAddr,
Claimer: claimerAddr,
PubKeyClaim: cmt,
PubKeyRefund: refundKey,
Timeout0: t0,
@@ -84,7 +101,7 @@ func Test_ValidateAndSendTransaction(t *testing.T) {
}
// set contract to Ready
tx, err = swapCreator.SetReady(txOpts, *swap)
tx, err = swapCreator.SetReady(txOpts, swap)
require.NoError(t, err)
receipt, err = block.WaitForReceipt(ctx, ec.Raw(), tx.Hash())
require.NoError(t, err)
@@ -92,15 +109,32 @@ func Test_ValidateAndSendTransaction(t *testing.T) {
secret := proof.Secret()
// generate relayer hash
var salt [4]byte
_, err = rand.Read(salt[:])
require.NoError(t, err)
relayerHash := crypto.Keccak256Hash(relayerAddr[:], salt[:])
// now let's try to claim
req, err := CreateRelayClaimRequest(ctx, sk, ec.Raw(), swapCreatorAddr, forwarderAddr, swap, &secret)
relaySwap := &contracts.SwapCreatorRelaySwap{
Swap: swap,
SwapCreator: swapCreatorAddr,
RelayerHash: relayerHash,
Fee: big.NewInt(1),
}
req, err := CreateRelayClaimRequest(claimerSk, relaySwap, secret)
require.NoError(t, err)
resp, err := ValidateAndSendTransaction(ctx, req, ec, swapCreatorAddr)
resp, err := ValidateAndSendTransaction(ctx, req, ec, swapCreatorAddr, salt)
require.NoError(t, err)
receipt, err = block.WaitForReceipt(ctx, ec.Raw(), resp.TxHash)
require.NoError(t, err)
t.Logf("gas cost to call claimRelayer: %d (delta %d)",
receipt.GasUsed, maxClaimRelayerETHGas-int(receipt.GasUsed))
require.GreaterOrEqual(t, maxClaimRelayerETHGas, int(receipt.GasUsed), "claimRelayer")
t.Logf("gas cost to call Claim via relayer: %d", receipt.GasUsed)
// expected 1 Claimed log (ERC20 swaps have 3, but we don't support relaying with ERC20 swaps)
@@ -110,11 +144,11 @@ func Test_ValidateAndSendTransaction(t *testing.T) {
require.NoError(t, err)
require.Equal(t, contracts.StageCompleted, stage)
// Now lets try to claim a second time and verify that we fail on the simulated
// Now let's try to claim a second time and verify that we fail on the simulated
// execution.
req, err = CreateRelayClaimRequest(ctx, sk, ec.Raw(), swapCreatorAddr, forwarderAddr, swap, &secret)
req, err = CreateRelayClaimRequest(claimerSk, relaySwap, secret)
require.NoError(t, err)
_, err = ValidateAndSendTransaction(ctx, req, ec, swapCreatorAddr)
require.ErrorContains(t, err, "relayed transaction failed on simulation")
_, err = ValidateAndSendTransaction(ctx, req, ec, swapCreatorAddr, salt)
require.ErrorContains(t, err, "revert")
}

View File

@@ -7,9 +7,8 @@ import (
"context"
"fmt"
"github.com/athanorlabs/go-relayer/impls/gsnforwarder"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/athanorlabs/atomic-swap/coins"
@@ -22,52 +21,63 @@ func validateClaimRequest(
ctx context.Context,
request *message.RelayClaimRequest,
ec *ethclient.Client,
ourSFContractAddr ethcommon.Address,
ourAddress ethcommon.Address,
salt [4]byte,
ourSwapCreatorAddr ethcommon.Address,
) error {
err := validateClaimValues(ctx, request, ec, ourSFContractAddr)
err := validateClaimValues(ctx, request, ec, ourAddress, salt, ourSwapCreatorAddr)
if err != nil {
return err
}
return validateClaimSignature(ctx, ec, request)
return validateClaimSignature(request)
}
// validateClaimValues validates the non-signature aspects of the claim request:
// 1. the claim request's swap creator and forwarder contract bytecode matches ours
// 1. the claim request's SwapCreator bytecode matches ours
// 2. the swap is for ETH and not an ERC20 token
// 3. the swap value is strictly greater than the relayer fee
// 4. TODO: Validate that the swap exists and is in a claimable state?
// 4. the claim request's relayer hash matches keccak256(ourAddress || salt)
func validateClaimValues(
ctx context.Context,
request *message.RelayClaimRequest,
ec *ethclient.Client,
ourAddress ethcommon.Address,
salt [4]byte,
ourSwapCreatorAddr ethcommon.Address,
) error {
isTakerRelay := request.OfferID != nil
// Validate the deployed SwapCreator contract, if it is not at the same address
// as our own. The CheckSwapCreatorContractCode method validates both the
// SwapCreator bytecode and the Forwarder bytecode.
if request.SwapCreatorAddr != ourSwapCreatorAddr {
// Validate the requested SwapCreator contract, if it is not at the same address
// as our own.
if request.RelaySwap.SwapCreator != ourSwapCreatorAddr {
if isTakerRelay {
return fmt.Errorf("taker claim swap creator mismatch found=%s expected=%s",
request.SwapCreatorAddr, ourSwapCreatorAddr)
request.RelaySwap.SwapCreator, ourSwapCreatorAddr)
}
_, err := contracts.CheckSwapCreatorContractCode(ctx, ec, request.SwapCreatorAddr)
err := contracts.CheckSwapCreatorContractCode(ctx, ec, request.RelaySwap.SwapCreator)
if err != nil {
return err
}
}
asset := types.EthAsset(request.Swap.Asset)
asset := types.EthAsset(request.RelaySwap.Swap.Asset)
if asset != types.EthAssetETH {
return fmt.Errorf("relaying for ETH Asset %s is not supported", asset)
}
// The relayer fee must be strictly less than the swap value
if coins.RelayerFeeWei.Cmp(request.Swap.Value) >= 0 {
if coins.RelayerFeeWei.Cmp(request.RelaySwap.Swap.Value) >= 0 {
return fmt.Errorf("swap value of %s ETH is too low to support %s ETH relayer fee",
coins.FmtWeiAsETH(request.Swap.Value), coins.FmtWeiAsETH(coins.RelayerFeeWei))
coins.FmtWeiAsETH(request.RelaySwap.Swap.Value), coins.RelayerFeeETH.Text('f'))
}
hash := ethcrypto.Keccak256Hash(append(ourAddress.Bytes(), salt[:]...))
if request.RelaySwap.RelayerHash != hash {
return fmt.Errorf("relay request payout address hash %s does not match expected (%s)",
request.RelaySwap.RelayerHash,
hash,
)
}
return nil
@@ -76,57 +86,25 @@ func validateClaimValues(
// validateClaimSignature validates the claim signature. It is assumed that the
// request fields have already been validated.
func validateClaimSignature(
ctx context.Context,
ec *ethclient.Client,
request *message.RelayClaimRequest,
) error {
callOpts := &bind.CallOpts{
Context: ctx,
From: ethcommon.Address{0xFF}, // can be any value but zero, which will validate all signatures
}
msg := request.RelaySwap.Hash()
var sig [65]byte
copy(sig[:], request.Signature)
sig[64] -= 27 // ecrecover requires 0/1 while EVM requires 27/28
swapCreator, err := contracts.NewSwapCreator(request.SwapCreatorAddr, ec)
signer, err := ethcrypto.Ecrecover(msg[:], sig[:])
if err != nil {
return err
}
forwarderAddr, err := swapCreator.TrustedForwarder(&bind.CallOpts{Context: ctx})
pubkey, err := ethcrypto.UnmarshalPubkey(signer)
if err != nil {
return err
}
forwarder, domainSeparator, err := getForwarderAndDomainSeparator(ctx, ec, forwarderAddr)
if err != nil {
return err
}
nonce, err := forwarder.GetNonce(callOpts, request.Swap.Claimer)
if err != nil {
return err
}
secret := (*[32]byte)(request.Secret)
forwarderRequest, err := createForwarderRequest(
nonce,
request.SwapCreatorAddr,
request.Swap,
secret,
)
if err != nil {
return err
}
err = forwarder.Verify(
callOpts,
*forwarderRequest,
*domainSeparator,
gsnforwarder.ForwardRequestTypehash,
nil,
request.Signature,
)
if err != nil {
return fmt.Errorf("failed to verify signature: %w", err)
if ethcrypto.PubkeyToAddress(*pubkey) != request.RelaySwap.Swap.Claimer {
return fmt.Errorf("signer of message is not swap claimer")
}
return nil

View File

@@ -25,7 +25,11 @@ func TestValidateRelayerFee(t *testing.T) {
ctx := context.Background()
ec, _ := tests.NewEthClient(t)
key := tests.GetTakerTestKey(t)
swapCreatorAddr, _ := deployContracts(t, ec, key)
swapCreatorAddr := deployContracts(t, ec, key)
// 20-byte empty address, 4-byte zero salt
empty := [24]byte{}
relayerHash := crypto.Keccak256Hash(empty[:])
type testCase struct {
description string
@@ -51,7 +55,7 @@ func TestValidateRelayerFee(t *testing.T) {
}
for _, tc := range testCases {
swap := &contracts.SwapCreatorSwap{
swap := contracts.SwapCreatorSwap{
Owner: ethcommon.Address{},
Claimer: ethcommon.Address{},
PubKeyClaim: [32]byte{},
@@ -64,12 +68,16 @@ func TestValidateRelayerFee(t *testing.T) {
}
request := &message.RelayClaimRequest{
SwapCreatorAddr: swapCreatorAddr,
Swap: swap,
Secret: make([]byte, 32),
RelaySwap: &contracts.SwapCreatorRelaySwap{
Swap: swap,
Fee: big.NewInt(1),
SwapCreator: swapCreatorAddr,
RelayerHash: relayerHash,
},
Secret: make([]byte, 32),
}
err := validateClaimValues(ctx, request, ec, swapCreatorAddr)
err := validateClaimValues(ctx, request, ec, ethcommon.Address{}, [4]byte{}, swapCreatorAddr)
if tc.expectErr != "" {
require.ErrorContains(t, err, tc.expectErr, tc.description)
} else {
@@ -87,13 +95,14 @@ func Test_validateClaimValues_takerClaim_contractAddressNotEqualFail(t *testing.
swapCreatorAddrOurs := ethcommon.Address{0x2} // passed to validateClaimValues
request := &message.RelayClaimRequest{
OfferID: &offerID,
SwapCreatorAddr: swapCreatorAddrInClaim,
Secret: make([]byte, 32),
Swap: new(contracts.SwapCreatorSwap), // test fails before we validate this
OfferID: &offerID,
Secret: make([]byte, 32),
RelaySwap: &contracts.SwapCreatorRelaySwap{
SwapCreator: swapCreatorAddrInClaim,
},
}
err := validateClaimValues(context.Background(), request, nil, swapCreatorAddrOurs)
err := validateClaimValues(context.Background(), request, nil, ethcommon.Address{}, [4]byte{}, swapCreatorAddrOurs)
require.ErrorContains(t, err, "taker claim swap creator mismatch")
}
@@ -103,39 +112,47 @@ func Test_validateClaimValues_takerClaim_contractAddressNotEqualFail(t *testing.
func Test_validateClaimValues_dhtClaim_contractAddressNotEqual(t *testing.T) {
ec, _ := tests.NewEthClient(t)
key := tests.GetTakerTestKey(t)
swapCreatorAddr, forwarderAddr := deployContracts(t, ec, key)
swapCreatorAddr := deployContracts(t, ec, key)
request := &message.RelayClaimRequest{
OfferID: nil, // DHT relayer claim
SwapCreatorAddr: forwarderAddr, // not a valid swap creator contract
Secret: make([]byte, 32),
Swap: new(contracts.SwapCreatorSwap), // test fails before we validate this
OfferID: nil, // DHT relayer claim
Secret: make([]byte, 32),
RelaySwap: &contracts.SwapCreatorRelaySwap{
SwapCreator: ethcommon.Address{1}, // not a valid swap creator contract
},
}
err := validateClaimValues(context.Background(), request, ec, swapCreatorAddr)
err := validateClaimValues(context.Background(), request, ec, ethcommon.Address{}, [4]byte{}, swapCreatorAddr)
require.ErrorContains(t, err, "contract address does not contain correct SwapCreator code")
}
func Test_validateSignature(t *testing.T) {
ctx := context.Background()
ethKey := tests.GetMakerTestKey(t)
claimer := crypto.PubkeyToAddress(*ethKey.Public().(*ecdsa.PublicKey))
ec, _ := tests.NewEthClient(t)
secret := [32]byte{0x1}
swapCreatorAddr, forwarderAddr := deployContracts(t, ec, ethKey)
swapCreatorAddr := deployContracts(t, ec, ethKey)
swap := createTestSwap(claimer)
req, err := CreateRelayClaimRequest(ctx, ethKey, ec, swapCreatorAddr, forwarderAddr, swap, &secret)
relaySwap := &contracts.SwapCreatorRelaySwap{
SwapCreator: swapCreatorAddr,
Swap: *swap,
RelayerHash: types.Hash{},
Fee: big.NewInt(1),
}
req, err := CreateRelayClaimRequest(ethKey, relaySwap, secret)
require.NoError(t, err)
// success path
err = validateClaimSignature(ctx, ec, req)
err = validateClaimSignature(req)
require.NoError(t, err)
// failure path (tamper with an arbitrary byte of the signature)
req.Signature[10]++
err = validateClaimSignature(ctx, ec, req)
require.ErrorContains(t, err, "failed to verify signature")
err = validateClaimSignature(req)
// can be "recovery failed" or "signer of message is not swap claimer"
require.Error(t, err)
}
func Test_validateClaimRequest(t *testing.T) {
@@ -144,19 +161,30 @@ func Test_validateClaimRequest(t *testing.T) {
claimer := crypto.PubkeyToAddress(*ethKey.Public().(*ecdsa.PublicKey))
ec, _ := tests.NewEthClient(t)
secret := [32]byte{0x1}
swapCreatorAddr, forwarderAddr := deployContracts(t, ec, ethKey)
swapCreatorAddr := deployContracts(t, ec, ethKey)
// 20-byte empty address, 4-byte zero salt
empty := [24]byte{}
relayerHash := crypto.Keccak256Hash(empty[:])
swap := createTestSwap(claimer)
req, err := CreateRelayClaimRequest(ctx, ethKey, ec, swapCreatorAddr, forwarderAddr, swap, &secret)
relaySwap := &contracts.SwapCreatorRelaySwap{
SwapCreator: swapCreatorAddr,
Swap: *swap,
RelayerHash: relayerHash,
Fee: big.NewInt(1),
}
req, err := CreateRelayClaimRequest(ethKey, relaySwap, secret)
require.NoError(t, err)
// success path
err = validateClaimRequest(ctx, req, ec, swapCreatorAddr)
err = validateClaimRequest(ctx, req, ec, ethcommon.Address{}, [4]byte{}, swapCreatorAddr)
require.NoError(t, err)
// test failure path by passing a non-eth asset
asset := ethcommon.Address{0x1}
req.Swap.Asset = asset
err = validateClaimRequest(ctx, req, ec, swapCreatorAddr)
req.RelaySwap.Swap.Asset = asset
err = validateClaimRequest(ctx, req, ec, ethcommon.Address{}, [4]byte{}, swapCreatorAddr)
require.ErrorContains(t, err, fmt.Sprintf("relaying for ETH Asset %s is not supported", types.EthAsset(asset)))
}

View File

@@ -28,6 +28,6 @@ compile-contract() {
}
compile-contract SwapCreator SwapCreator swap_creator
compile-contract TestERC20 TestERC20 erc20_mock
compile-contract TestERC20 TestERC20 erc20_token
compile-contract IERC20Metadata IERC20 ierc20
compile-contract AggregatorV3Interface AggregatorV3Interface aggregator_v3_interface

View File

@@ -106,8 +106,7 @@ start-daemons() {
fi
SWAP_CREATOR_ADDR="$(jq -r .swapCreatorAddr "${CONTRACT_ADDR_FILE}")"
FORWARDER_ADDR="$(jq -r .forwarderAddr "${CONTRACT_ADDR_FILE}")"
if [[ -z "${SWAP_CREATOR_ADDR}" ]] || [[ -z "${FORWARDER_ADDR}" ]]; then
if [[ -z "${SWAP_CREATOR_ADDR}" ]]; then
echo "Failed to get Alice's deployed contract addresses"
stop-daemons
exit 1

View File

@@ -25,7 +25,6 @@ func (s *IntegrationTestSuite) TestXMRMaker_DiscoverRelayer() {
ctx := context.Background()
c := rpcclient.NewClient(ctx, defaultXMRMakerSwapdEndpoint)
// see https://github.com/AthanorLabs/go-relayer/blob/master/net/host.go#L20
peerIDs, err := c.Discover("relayer", defaultDiscoverTimeout)
require.NoError(s.T(), err)
require.Equal(s.T(), 1, len(peerIDs))