add relayer support for claim contract function (#204)

This commit is contained in:
noot
2022-10-26 23:39:20 -04:00
committed by GitHub
parent 5c32755778
commit 0bdb6f6f2a
51 changed files with 1470 additions and 427 deletions

View File

@@ -65,6 +65,7 @@ type Backend interface {
TxOpts() (*bind.TransactOpts, error)
SwapManager() swap.Manager
EthAddress() ethcommon.Address
EthPrivateKey() *ecdsa.PrivateKey
Contract() *contracts.SwapFactory
ContractAddr() ethcommon.Address
Net() net.MessageSender
@@ -80,8 +81,6 @@ type Backend interface {
SetXMRDepositAddress(mcrypto.Address, types.Hash)
ClearXMRDepositAddress(types.Hash)
SetBaseXMRDepositAddress(mcrypto.Address)
SetContract(*contracts.SwapFactory)
SetContractAddress(ethcommon.Address)
}
type backend struct {
@@ -228,6 +227,10 @@ func (b *backend) EthClient() *ethclient.Client {
return b.ethClient
}
func (b *backend) EthPrivateKey() *ecdsa.PrivateKey {
return b.ethPrivKey
}
func (b *backend) Net() net.MessageSender {
return b.MessageSender
}
@@ -398,15 +401,3 @@ func (b *backend) ClearXMRDepositAddress(id types.Hash) {
defer b.Unlock()
delete(b.xmrDepositAddrs, id)
}
// NOTE: this is called when a swap is initiated and the XMR-taker specifies the contract
// address they will be using.
// the contract bytecode is validated in the calling code, but this should never be called
// for unvalidated contracts.
func (b *backend) SetContract(contract *contracts.SwapFactory) {
b.contract = contract
}
func (b *backend) SetContractAddress(addr ethcommon.Address) {
b.contractAddr = addr
}

52
protocol/check.go Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,7 @@
package xmrmaker
package protocol
import (
"context"
"math/big"
"testing"
contracts "github.com/athanorlabs/atomic-swap/ethereum"
@@ -10,15 +9,10 @@ import (
"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"
)
func TestCheckContractCode(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
b := NewMockBackend(ctrl)
ec, chainID := tests.NewEthClient(t)
ctx := context.Background()
pk := tests.GetMakerTestKey(t)
@@ -26,17 +20,26 @@ func TestCheckContractCode(t *testing.T) {
txOpts, err := bind.NewKeyedTransactorWithChainID(pk, chainID)
require.NoError(t, err)
_, tx, _, err := contracts.DeploySwapFactory(txOpts, ec)
_, tx, _, err := contracts.DeploySwapFactory(txOpts, ec, ethcommon.Address{})
require.NoError(t, err)
addr, err := bind.WaitDeployed(ctx, ec, tx)
require.NoError(t, err)
b.EXPECT().CodeAt(context.Background(), addr, nil).
DoAndReturn(func(ctx context.Context, account ethcommon.Address, _ *big.Int) ([]byte, error) {
return ec.CodeAt(ctx, account, nil)
})
err = CheckContractCode(ctx, ec, addr)
require.NoError(t, err)
err = checkContractCode(ctx, b, addr)
// deploy with some arbitrary trustedForwarder address
_, tx, _, err = contracts.DeploySwapFactory(
txOpts,
ec,
ethcommon.HexToAddress("0x64e902cD8A29bBAefb9D4e2e3A24d8250C606ee7"),
)
require.NoError(t, err)
addr, err = bind.WaitDeployed(ctx, ec, tx)
require.NoError(t, err)
err = CheckContractCode(ctx, ec, addr)
require.NoError(t, err)
}

View File

@@ -8,10 +8,15 @@ import (
)
// MakeOffer makes a new swap offer.
func (b *Instance) MakeOffer(o *types.Offer) (*types.OfferExtra, error) {
func (b *Instance) MakeOffer(
o *types.Offer,
relayerEndpoint string,
relayerCommission float64,
) (*types.OfferExtra, error) {
b.backend.LockClient()
defer b.backend.UnlockClient()
// get monero balance
balance, err := b.backend.GetBalance(0)
if err != nil {
return nil, err
@@ -22,7 +27,7 @@ func (b *Instance) MakeOffer(o *types.Offer) (*types.OfferExtra, error) {
return nil, errUnlockedBalanceTooLow{unlockedBalance.AsMonero(), o.MaximumAmount}
}
extra, err := b.offerManager.AddOffer(o)
extra, err := b.offerManager.AddOffer(o, relayerEndpoint, relayerCommission)
if err != nil {
return nil, err
}

223
protocol/xmrmaker/claim.go Normal file
View File

@@ -0,0 +1,223 @@
package xmrmaker
import (
"context"
"crypto/ecdsa"
"fmt"
"math"
"math/big"
"strings"
"time"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"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/relayer"
)
var numEtherUnitsFloat = big.NewFloat(math.Pow(10, 18))
func (s *swapState) tryClaim() (ethcommon.Hash, error) {
stage, err := s.Contract().Swaps(s.CallOpts(), s.contractSwapID)
if err != nil {
return ethcommon.Hash{}, err
}
switch stage {
case contracts.StageInvalid:
return ethcommon.Hash{}, errClaimInvalid
case contracts.StageCompleted:
return ethcommon.Hash{}, errClaimSwapComplete
case contracts.StagePending, contracts.StageReady:
// do nothing
default:
panic("Unhandled stage value")
}
ts, err := s.LatestBlockTimestamp(s.ctx)
if err != nil {
return ethcommon.Hash{}, err
}
// The block that our claim transaction goes into needs a timestamp that is strictly less
// than T1. Since the minimum interval between blocks is 1 second, the current block must
// be at least 2 seconds before T1 for a non-zero chance of the next block having a
// timestamp that is strictly less than T1.
if ts.After(s.t1.Add(-2 * time.Second)) {
// We've passed t1, so the only way we can regain control of the locked XMR is for
// XMRTaker to call refund on the contract.
return ethcommon.Hash{}, errClaimPastTime
}
if ts.Before(s.t0) && stage != contracts.StageReady {
// TODO: t0 could be 24 hours from now. Don't we want to poll the stage periodically? (#163)
// we need to wait until t0 to claim
log.Infof("waiting until time %s to claim, time now=%s", s.t0, time.Now())
err = s.WaitForTimestamp(s.ctx, s.t0)
if err != nil {
return ethcommon.Hash{}, err
}
}
return s.claimFunds()
}
// claimFunds redeems XMRMaker's ETH funds by calling Claim() on the contract
func (s *swapState) claimFunds() (ethcommon.Hash, error) {
addr := s.EthAddress()
var (
symbol string
decimals uint8
err error
)
if types.EthAsset(s.contractSwap.Asset) != types.EthAssetETH {
_, symbol, decimals, err = s.ERC20Info(s.ctx, s.contractSwap.Asset)
if err != nil {
return ethcommon.Hash{}, fmt.Errorf("failed to get ERC20 info: %w", err)
}
}
if types.EthAsset(s.contractSwap.Asset) == types.EthAssetETH {
balance, err := s.BalanceAt(s.ctx, addr, nil) //nolint:govet
if err != nil {
return ethcommon.Hash{}, err
}
log.Infof("balance before claim: %v ETH", common.EtherAmount(*balance).AsEther())
} else {
balance, err := s.ERC20BalanceAt(s.ctx, s.contractSwap.Asset, addr, nil) //nolint:govet
if err != nil {
return ethcommon.Hash{}, err
}
log.Infof("balance before claim: %v %s", common.EtherAmount(*balance).ToDecimals(decimals), symbol)
}
var (
txHash ethcommon.Hash
)
// call swap.Swap.Claim() w/ b.privkeys.sk, revealing XMRMaker's secret spend key
if s.offerExtra.RelayerEndpoint != "" {
// relayer endpoint is set, claim using relayer
// TODO: eventually update when relayer discovery is implemented
txHash, err = s.claimRelayer()
if err != nil {
return ethcommon.Hash{}, err
}
} else {
// claim and wait for tx to be included
sc := s.getSecret()
txHash, _, err = s.sender.Claim(s.contractSwap, sc)
if err != nil {
return ethcommon.Hash{}, err
}
}
log.Infof("sent claim transaction, tx hash=%s", txHash)
if types.EthAsset(s.contractSwap.Asset) == types.EthAssetETH {
balance, err := s.BalanceAt(s.ctx, addr, nil)
if err != nil {
return ethcommon.Hash{}, err
}
log.Infof("balance after claim: %v ETH", common.EtherAmount(*balance).AsEther())
} else {
balance, err := s.ERC20BalanceAt(s.ctx, s.contractSwap.Asset, addr, nil)
if err != nil {
return ethcommon.Hash{}, err
}
log.Infof("balance after claim: %v %s", common.EtherAmount(*balance).ToDecimals(decimals), symbol)
}
return txHash, nil
}
func (s *swapState) claimRelayer() (ethcommon.Hash, error) {
return claimRelayer(
s.Ctx(),
s.EthPrivateKey(),
s.Contract(),
s.contractAddr,
s.EthClient(),
s.offerExtra.RelayerEndpoint,
s.offerExtra.RelayerCommission,
&s.contractSwap,
s.getSecret(),
)
}
// claimRelayer claims the ETH funds via relayer.
func claimRelayer(
ctx context.Context,
sk *ecdsa.PrivateKey,
contract *contracts.SwapFactory,
contractAddr ethcommon.Address,
ec *ethclient.Client,
relayerEndpoint string,
relayerCommission float64,
contractSwap *contracts.SwapFactorySwap,
secret [32]byte,
) (ethcommon.Hash, error) {
forwarderAddress, err := contract.TrustedForwarder(&bind.CallOpts{})
if err != nil {
return ethcommon.Hash{}, err
}
rc, err := relayer.NewClient(sk, ec, relayerEndpoint, forwarderAddress)
if err != nil {
return ethcommon.Hash{}, err
}
abi, err := abi.JSON(strings.NewReader(contracts.SwapFactoryABI))
if err != nil {
return ethcommon.Hash{}, err
}
feeValue, err := calculateRelayerCommissionValue(contractSwap.Value, relayerCommission)
if err != nil {
return ethcommon.Hash{}, err
}
calldata, err := abi.Pack("claimRelayer", *contractSwap, secret, feeValue)
if err != nil {
return ethcommon.Hash{}, err
}
txHash, err := rc.SubmitTransaction(contractAddr, calldata)
if err != nil {
return ethcommon.Hash{}, err
}
// wait for inclusion
receipt, err := block.WaitForReceipt(ctx, ec, txHash)
if err != nil {
return ethcommon.Hash{}, err
}
if receipt.Status == 0 {
return ethcommon.Hash{}, fmt.Errorf("transaction failed")
}
return txHash, nil
}
// swapValue is in wei
// relayerCommission is a percentage (ie must be much less than 1)
// error if it's greater than 0.1 (10%) - arbitrary, just a sanity check
func calculateRelayerCommissionValue(swapValue *big.Int, relayerCommission float64) (*big.Int, error) {
if relayerCommission > 0.1 {
return nil, errRelayerCommissionTooHigh
}
swapValueF := big.NewFloat(0).SetInt(swapValue)
relayerCommissionF := big.NewFloat(relayerCommission)
feeValue := big.NewFloat(0).Mul(swapValueF, relayerCommissionF)
wei, _ := feeValue.Int(nil)
return wei, nil
}

View File

@@ -0,0 +1,197 @@
package xmrmaker
import (
"context"
"crypto/ecdsa"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/stretchr/testify/require"
rcommon "github.com/AthanorLabs/go-relayer/common"
"github.com/AthanorLabs/go-relayer/impls/gsnforwarder"
"github.com/AthanorLabs/go-relayer/relayer"
rrpc "github.com/AthanorLabs/go-relayer/rpc"
"github.com/athanorlabs/atomic-swap/common"
"github.com/athanorlabs/atomic-swap/common/types"
"github.com/athanorlabs/atomic-swap/dleq"
contracts "github.com/athanorlabs/atomic-swap/ethereum"
"github.com/athanorlabs/atomic-swap/ethereum/block"
"github.com/athanorlabs/atomic-swap/tests"
)
var (
defaultTestTimeoutDuration = big.NewInt(60 * 5)
defaultRelayerEndpoint = "http://127.0.0.1:7799"
relayerCommission = float64(0.01)
)
func runRelayer(
t *testing.T,
ec *ethclient.Client,
forwarderAddress ethcommon.Address,
sk *ecdsa.PrivateKey,
chainID *big.Int,
) {
iforwarder, err := gsnforwarder.NewIForwarder(forwarderAddress, ec)
require.NoError(t, err)
fw := gsnforwarder.NewIForwarderWrapped(iforwarder)
key := rcommon.NewKeyFromPrivateKey(sk)
cfg := &relayer.Config{
Ctx: context.Background(),
EthClient: ec,
Forwarder: fw,
Key: key,
ChainID: chainID,
NewForwardRequestFunc: gsnforwarder.NewIForwarderForwardRequest,
}
r, err := relayer.NewRelayer(cfg)
require.NoError(t, err)
rpcCfg := &rrpc.Config{
Port: 7799,
Relayer: r,
}
server, err := rrpc.NewServer(rpcCfg)
require.NoError(t, err)
_ = server.Start()
t.Cleanup(func() {
// TODO stop server
})
}
func TestSwapState_ClaimRelayer(t *testing.T) {
sk := tests.GetMakerTestKey(t)
relayerSk := tests.GetTestKeyByIndex(t, 1)
require.NotEqual(t, sk, relayerSk)
conn, chainID := tests.NewEthClient(t)
txOpts, err := bind.NewKeyedTransactorWithChainID(sk, chainID)
require.NoError(t, err)
// generate claim secret and public key
dleq := &dleq.CGODLEq{}
proof, err := dleq.Prove()
require.NoError(t, err)
res, err := dleq.Verify(proof)
require.NoError(t, err)
// hash public key of claim secret
cmt := res.Secp256k1PublicKey().Keccak256()
pub := sk.Public().(*ecdsa.PublicKey)
addr := crypto.PubkeyToAddress(*pub)
// deploy forwarder
forwarderAddress, tx, forwarderContract, err := gsnforwarder.DeployForwarder(txOpts, conn)
require.NoError(t, err)
receipt, err := block.WaitForReceipt(context.Background(), conn, tx.Hash())
require.NoError(t, err)
t.Logf("gas cost to deploy Forwarder.sol: %d", receipt.GasUsed)
tx, err = forwarderContract.RegisterDomainSeparator(txOpts, gsnforwarder.DefaultName, gsnforwarder.DefaultVersion)
require.NoError(t, err)
receipt, err = block.WaitForReceipt(context.Background(), conn, tx.Hash())
require.NoError(t, err)
t.Logf("gas cost to call RegisterDomainSeparator: %d", receipt.GasUsed)
// start relayer
runRelayer(t, conn, forwarderAddress, relayerSk, chainID)
// deploy swap contract with claim key hash
contractAddr, tx, contract, err := contracts.DeploySwapFactory(txOpts, conn, forwarderAddress)
require.NoError(t, err)
receipt, err = block.WaitForReceipt(context.Background(), conn, tx.Hash())
require.NoError(t, err)
t.Logf("gas cost to deploy SwapFactory.sol: %d", receipt.GasUsed)
value := big.NewInt(100000000000)
nonce := big.NewInt(0)
txOpts.Value = value
tx, err = contract.NewSwap(txOpts, cmt, [32]byte{}, addr,
defaultTestTimeoutDuration, types.EthAssetETH.Address(), value, nonce)
require.NoError(t, err)
receipt, err = block.WaitForReceipt(context.Background(), conn, tx.Hash())
require.NoError(t, err)
t.Logf("gas cost to call new_swap: %d", receipt.GasUsed)
txOpts.Value = big.NewInt(0)
require.Equal(t, 1, len(receipt.Logs))
id, err := contracts.GetIDFromLog(receipt.Logs[0])
require.NoError(t, err)
t0, t1, err := contracts.GetTimeoutsFromLog(receipt.Logs[0])
require.NoError(t, err)
swap := contracts.SwapFactorySwap{
Owner: addr,
Claimer: addr,
PubKeyClaim: cmt,
PubKeyRefund: [32]byte{},
Timeout0: t0,
Timeout1: t1,
Asset: types.EthAssetETH.Address(),
Value: value,
Nonce: nonce,
}
// set contract to Ready
tx, err = contract.SetReady(txOpts, swap)
require.NoError(t, err)
receipt, err = block.WaitForReceipt(context.Background(), conn, tx.Hash())
t.Logf("gas cost to call SetReady: %d", receipt.GasUsed)
require.NoError(t, err)
// now let's try to claim
var s [32]byte
secret := proof.Secret()
copy(s[:], common.Reverse(secret[:]))
txHash, err := claimRelayer(
context.Background(),
sk,
contract,
contractAddr,
conn,
defaultRelayerEndpoint,
relayerCommission,
&swap,
s,
)
require.NoError(t, err)
receipt, err = block.WaitForReceipt(context.Background(), conn, txHash)
require.NoError(t, err)
t.Logf("gas cost to call Claim via relayer: %d", receipt.GasUsed)
// expected 1 Claimed log
require.Equal(t, 1, len(receipt.Logs))
stage, err := contract.Swaps(nil, id)
require.NoError(t, err)
require.Equal(t, contracts.StageCompleted, stage)
}
func TestCalculateRelayerCommissionValue(t *testing.T) {
swapValueF := big.NewFloat(0).Mul(big.NewFloat(4.567), numEtherUnitsFloat)
swapValue, _ := swapValueF.Int(nil)
relayerCommission := float64(0.01398)
expectedF := big.NewFloat(0).Mul(big.NewFloat(0.06384666), numEtherUnitsFloat)
expected, _ := expectedF.Int(nil)
val, err := calculateRelayerCommissionValue(swapValue, relayerCommission)
require.NoError(t, err)
require.Equal(t, expected, val)
}

View File

@@ -24,10 +24,10 @@ var (
errClaimTxHasNoLogs = errors.New("claim transaction has no logs")
errCannotFindNewLog = errors.New("cannot find New log")
errUnexpectedSwapID = errors.New("unexpected swap ID was emitted by New log")
errInvalidSwapContract = errors.New("given contract address does not contain correct code")
errSwapIDMismatch = errors.New("hash of swap struct does not match swap ID")
errLockTxReverted = errors.New("other party failed to lock ETH asset (transaction reverted)")
errInvalidETHLockedTransaction = errors.New("eth locked tx was not to correct contract address")
errRelayerCommissionTooHigh = errors.New("relayer commission must be less than 0.1 (10%)")
// protocol initiation errors
errProtocolAlreadyInProgress = errors.New("protocol already in progress")

View File

@@ -84,8 +84,8 @@ func (s *swapState) HandleProtocolMessage(msg net.Message) (net.Message, bool, e
func (s *swapState) clearNextExpectedMessage(status types.Status) {
s.nextExpectedMessage = nil
s.info.SetStatus(status)
if s.statusCh != nil {
s.statusCh <- status
if s.offerExtra.StatusCh != nil {
s.offerExtra.StatusCh <- status
}
}
@@ -104,8 +104,8 @@ func (s *swapState) setNextExpectedMessage(msg net.Message) {
s.nextExpectedMessage = msg
stage := pcommon.GetStatus(msg.Type())
if s.statusCh != nil && stage != types.UnknownStatus {
s.statusCh <- stage
if s.offerExtra.StatusCh != nil && stage != types.UnknownStatus {
s.offerExtra.StatusCh <- stage
}
}
@@ -149,12 +149,12 @@ func (s *swapState) handleNotifyETHLocked(msg *message.NotifyETHLocked) (net.Mes
s.contractSwapID = msg.ContractSwapID
s.contractSwap = convertContractSwap(msg.ContractSwap)
if err := pcommon.WriteContractSwapToFile(s.infoFile, s.contractSwapID, s.contractSwap); err != nil {
if err := pcommon.WriteContractSwapToFile(s.offerExtra.InfoFile, s.contractSwapID, s.contractSwap); err != nil {
return nil, err
}
contractAddr := ethcommon.HexToAddress(msg.Address)
if err := checkContractCode(s.ctx, s, contractAddr); err != nil {
if err := pcommon.CheckContractCode(s.ctx, s.Backend.EthClient(), contractAddr); err != nil {
return nil, err
}
@@ -162,7 +162,7 @@ func (s *swapState) handleNotifyETHLocked(msg *message.NotifyETHLocked) (net.Mes
return nil, fmt.Errorf("failed to instantiate contract instance: %w", err)
}
if err := pcommon.WriteContractAddressToFile(s.infoFile, msg.Address); err != nil {
if err := pcommon.WriteContractAddressToFile(s.offerExtra.InfoFile, msg.Address); err != nil {
return nil, fmt.Errorf("failed to write contract address to file: %w", err)
}

View File

@@ -6,6 +6,7 @@ package xmrmaker
import (
context "context"
ecdsa "crypto/ecdsa"
big "math/big"
reflect "reflect"
time "time"
@@ -292,6 +293,20 @@ func (mr *MockBackendMockRecorder) EthClient() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthClient", reflect.TypeOf((*MockBackend)(nil).EthClient))
}
// EthPrivateKey mocks base method.
func (m *MockBackend) EthPrivateKey() *ecdsa.PrivateKey {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EthPrivateKey")
ret0, _ := ret[0].(*ecdsa.PrivateKey)
return ret0
}
// EthPrivateKey indicates an expected call of EthPrivateKey.
func (mr *MockBackendMockRecorder) EthPrivateKey() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthPrivateKey", reflect.TypeOf((*MockBackend)(nil).EthPrivateKey))
}
// FilterLogs mocks base method.
func (m *MockBackend) FilterLogs(arg0 context.Context, arg1 ethereum.FilterQuery) ([]types0.Log, error) {
m.ctrl.T.Helper()

View File

@@ -44,8 +44,7 @@ func (b *Instance) initiate(
// checks passed, delete offer for now
b.offerManager.DeleteOffer(offer.GetID())
s, err := newSwapState(b.backend, offer, b.offerManager, offerExtra.StatusCh,
offerExtra.InfoFile, providesAmount, desiredAmount)
s, err := newSwapState(b.backend, offer, offerExtra, b.offerManager, providesAmount, desiredAmount)
if err != nil {
return nil, err
}

View File

@@ -18,7 +18,7 @@ func TestXMRMaker_HandleInitiateMessage(t *testing.T) {
b.net.(*MockHost).EXPECT().Advertise()
_, err := b.MakeOffer(offer)
_, err := b.MakeOffer(offer, "", 0)
require.NoError(t, err)
msg, _ := newTestXMRTakerSendKeysMessage(t)

View File

@@ -78,7 +78,11 @@ func (m *Manager) GetOffer(id types.Hash) (*types.Offer, *types.OfferExtra, erro
}
// AddOffer adds a new offer to the manager and returns its OffersExtra data
func (m *Manager) AddOffer(o *types.Offer) (*types.OfferExtra, error) {
func (m *Manager) AddOffer(
o *types.Offer,
relayerEndpoint string,
relayerCommission float64,
) (*types.OfferExtra, error) {
m.mu.Lock()
defer m.mu.Unlock()

View File

@@ -29,7 +29,7 @@ func Test_Manager(t *testing.T) {
offer := types.NewOffer(types.ProvidesXMR, float64(i), float64(i), types.ExchangeRate(i),
types.EthAssetETH)
db.EXPECT().PutOffer(offer)
offerExtra, err := mgr.AddOffer(offer)
offerExtra, err := mgr.AddOffer(offer, "", 0)
require.NoError(t, err)
require.NotNil(t, offerExtra)
}

View File

@@ -50,7 +50,9 @@ func NewRecoveryState(b backend.Backend, dataDir string, secret *mcrypto.Private
dleqProof: dleq.NewProofWithSecret(sc),
contractSwapID: contractSwapID,
contractSwap: contractSwap,
infoFile: pcommon.GetSwapRecoveryFilepath(dataDir),
offerExtra: &types.OfferExtra{
InfoFile: pcommon.GetSwapRecoveryFilepath(dataDir),
},
}
if err := s.setContract(contractAddr); err != nil {

View File

@@ -39,15 +39,14 @@ type swapState struct {
backend.Backend
sender txsender.Sender
ctx context.Context
cancel context.CancelFunc
stateMu sync.Mutex
infoFile string
ctx context.Context
cancel context.CancelFunc
stateMu sync.Mutex
info *pswap.Info
offer *types.Offer
offerExtra *types.OfferExtra
offerManager *offers.Manager
statusCh chan types.Status
// our keys for this session
dleqProof *dleq.Proof
@@ -55,7 +54,9 @@ type swapState struct {
privkeys *mcrypto.PrivateKeyPair
pubkeys *mcrypto.PublicKeyPair
// swap contract and timeouts in it; set once contract is deployed
// swap contract and timeouts in it
contract *contracts.SwapFactory
contractAddr ethcommon.Address
contractSwapID [32]byte
contractSwap contracts.SwapFactorySwap
t0, t1 time.Time
@@ -79,20 +80,19 @@ type swapState struct {
func newSwapState(
b backend.Backend,
offer *types.Offer,
offerExtra *types.OfferExtra,
om *offers.Manager,
statusCh chan types.Status,
infoFile string,
providesAmount common.MoneroAmount,
desiredAmount common.EtherAmount,
) (*swapState, error) {
exchangeRate := types.ExchangeRate(providesAmount.AsMonero() / desiredAmount.AsEther())
stage := types.ExpectingKeys
if statusCh == nil {
statusCh = make(chan types.Status, 7)
if offerExtra.StatusCh == nil {
offerExtra.StatusCh = make(chan types.Status, 7)
}
statusCh <- stage
offerExtra.StatusCh <- stage
info := pswap.NewInfo(offer.GetID(), types.ProvidesXMR, providesAmount.AsMonero(), desiredAmount.AsEther(),
exchangeRate, offer.EthAsset, stage, statusCh)
exchangeRate, offer.EthAsset, stage, offerExtra.StatusCh)
if err := b.SwapManager().AddSwap(info); err != nil {
return nil, err
}
@@ -123,12 +123,11 @@ func newSwapState(
Backend: b,
sender: sender,
offer: offer,
offerExtra: offerExtra,
offerManager: om,
infoFile: infoFile,
nextExpectedMessage: &net.SendKeysMessage{},
readyCh: make(chan struct{}),
info: info,
statusCh: statusCh,
done: make(chan struct{}),
}
@@ -161,7 +160,7 @@ func (s *swapState) SendKeysMessage() (*net.SendKeysMessage, error) {
// InfoFile returns the swap's infoFile path
func (s *swapState) InfoFile() string {
return s.infoFile
return s.offerExtra.InfoFile
}
// ReceivedAmount returns the amount received, or expected to be received, at the end of the swap
@@ -208,7 +207,7 @@ func (s *swapState) exit() error {
if s.info.Status() != types.CompletedSuccess {
// re-add offer, as it wasn't taken successfully
_, err := s.offerManager.AddOffer(s.offer)
_, err := s.offerManager.AddOffer(s.offer, s.offerExtra.RelayerEndpoint, s.offerExtra.RelayerCommission)
if err != nil {
log.Warnf("failed to re-add offer %s: %s", s.offer.GetID(), err)
}
@@ -299,7 +298,7 @@ func (s *swapState) reclaimMonero(skA *mcrypto.PrivateSpendKey) (mcrypto.Address
kpAB := mcrypto.NewPrivateKeyPair(skAB, vkAB)
// write keys to file in case something goes wrong
if err = pcommon.WriteSharedSwapKeyPairToFile(s.infoFile, kpAB, s.Env()); err != nil {
if err = pcommon.WriteSharedSwapKeyPairToFile(s.offerExtra.InfoFile, kpAB, s.Env()); err != nil {
return "", err
}
@@ -353,50 +352,6 @@ func (s *swapState) filterForRefund() (*mcrypto.PrivateSpendKey, error) {
return sa, nil
}
func (s *swapState) tryClaim() (ethcommon.Hash, error) {
stage, err := s.Contract().Swaps(s.CallOpts(), s.contractSwapID)
if err != nil {
return ethcommon.Hash{}, err
}
switch stage {
case contracts.StageInvalid:
return ethcommon.Hash{}, errClaimInvalid
case contracts.StageCompleted:
return ethcommon.Hash{}, errClaimSwapComplete
case contracts.StagePending, contracts.StageReady:
// do nothing
default:
panic("Unhandled stage value")
}
ts, err := s.LatestBlockTimestamp(s.ctx)
if err != nil {
return ethcommon.Hash{}, err
}
// The block that our claim transaction goes into needs a timestamp that is strictly less
// than T1. Since the minimum interval between blocks is 1 second, the current block must
// be at least 2 seconds before T1 for a non-zero chance of the next block having a
// timestamp that is strictly less than T1.
if ts.After(s.t1.Add(-2 * time.Second)) {
// We've passed t1, so the only way we can regain control of the locked XMR is for
// XMRTaker to call refund on the contract.
return ethcommon.Hash{}, errClaimPastTime
}
if ts.Before(s.t0) && stage != contracts.StageReady {
// TODO: t0 could be 24 hours from now. Don't we want to poll the stage periodically? (#163)
// we need to wait until t0 to claim
log.Infof("waiting until time %s to claim, time now=%s", s.t0, time.Now())
err = s.WaitForTimestamp(s.ctx, s.t0)
if err != nil {
return ethcommon.Hash{}, err
}
}
return s.claimFunds()
}
// generateKeys generates XMRMaker's spend and view keys (s_b, v_b)
// It returns XMRMaker's public spend key and his private view key, so that XMRTaker can see
// if the funds are locked.
@@ -419,7 +374,7 @@ func (s *swapState) generateAndSetKeys() error {
s.privkeys = keysAndProof.PrivateKeyPair
s.pubkeys = keysAndProof.PublicKeyPair
return pcommon.WriteKeysToFile(s.infoFile, s.privkeys, s.Env())
return pcommon.WriteKeysToFile(s.offerExtra.InfoFile, s.privkeys, s.Env())
}
func generateKeys() (*pcommon.KeysAndProof, error) {
@@ -442,17 +397,16 @@ func (s *swapState) setXMRTakerPublicKeys(sk *mcrypto.PublicKeyPair, secp256k1Pu
// setContract sets the contract in which XMRTaker has locked her ETH.
func (s *swapState) setContract(address ethcommon.Address) error {
s.contractAddr = address
var err error
// note: this overrides the backend contract
s.SetContractAddress(address)
contract, err := s.NewSwapFactory(address)
s.contract, err = s.NewSwapFactory(address)
if err != nil {
return err
}
s.SetContract(contract)
s.sender.SetContractAddress(address)
s.sender.SetContract(contract)
s.sender.SetContract(s.contract)
return nil
}
@@ -470,7 +424,7 @@ func (s *swapState) checkContract(txHash ethcommon.Hash) error {
return err
}
if tx.To() == nil || *(tx.To()) != s.ContractAddr() {
if tx.To() == nil || *(tx.To()) != s.contractAddr {
return errInvalidETHLockedTransaction
}
@@ -570,60 +524,3 @@ func (s *swapState) lockFunds(amount common.MoneroAmount) (mcrypto.Address, erro
log.Infof("successfully locked XMR funds: address=%s", address)
return address, nil
}
// claimFunds redeems XMRMaker's ETH funds by calling Claim() on the contract
func (s *swapState) claimFunds() (ethcommon.Hash, error) {
addr := s.EthAddress()
var (
symbol string
decimals uint8
err error
)
if types.EthAsset(s.contractSwap.Asset) != types.EthAssetETH {
_, symbol, decimals, err = s.ERC20Info(s.ctx, s.contractSwap.Asset)
if err != nil {
return ethcommon.Hash{}, fmt.Errorf("failed to get ERC20 info: %w", err)
}
}
if types.EthAsset(s.contractSwap.Asset) == types.EthAssetETH {
balance, err := s.BalanceAt(s.ctx, addr, nil) //nolint:govet
if err != nil {
return ethcommon.Hash{}, err
}
log.Infof("balance before claim: %v ETH", common.EtherAmount(*balance).AsEther())
} else {
balance, err := s.ERC20BalanceAt(s.ctx, s.contractSwap.Asset, addr, nil) //nolint:govet
if err != nil {
return ethcommon.Hash{}, err
}
log.Infof("balance before claim: %v %s", common.EtherAmount(*balance).ToDecimals(decimals), symbol)
}
// call swap.Swap.Claim() w/ b.privkeys.sk, revealing XMRMaker's secret spend key
sc := s.getSecret()
txHash, _, err := s.sender.Claim(s.contractSwap, sc)
if err != nil {
return ethcommon.Hash{}, err
}
log.Infof("sent claim tx, tx hash=%s", txHash)
if types.EthAsset(s.contractSwap.Asset) == types.EthAssetETH {
balance, err := s.BalanceAt(s.ctx, addr, nil)
if err != nil {
return ethcommon.Hash{}, err
}
log.Infof("balance after claim: %v ETH", common.EtherAmount(*balance).AsEther())
} else {
balance, err := s.ERC20BalanceAt(s.ctx, s.contractSwap.Asset, addr, nil)
if err != nil {
return ethcommon.Hash{}, err
}
log.Infof("balance after claim: %v %s", common.EtherAmount(*balance).ToDecimals(decimals), symbol)
}
return txHash, nil
}

View File

@@ -63,7 +63,8 @@ func newTestXMRMakerAndDB(t *testing.T) (*Instance, *offers.MockDatabase) {
txOpts, err := bind.NewKeyedTransactorWithChainID(pk, chainID)
require.NoError(t, err)
_, tx, contract, err := contracts.DeploySwapFactory(txOpts, ec)
var forwarderAddress ethcommon.Address
_, tx, contract, err := contracts.DeploySwapFactory(txOpts, ec, forwarderAddress)
require.NoError(t, err)
addr, err := bind.WaitDeployed(context.Background(), ec, tx)
@@ -112,12 +113,14 @@ func newTestXMRMakerAndDB(t *testing.T) (*Instance, *offers.MockDatabase) {
func newTestInstanceAndDB(t *testing.T) (*Instance, *swapState, *offers.MockDatabase) {
xmrmaker, db := newTestXMRMakerAndDB(t)
infoFile := path.Join(t.TempDir(), "test.keys")
oe := &types.OfferExtra{
InfoFile: infoFile,
}
swapState, err := newSwapState(xmrmaker.backend,
types.NewOffer("", 0, 0, 0, types.EthAssetETH), xmrmaker.offerManager, nil, infoFile,
types.NewOffer("", 0, 0, 0, types.EthAssetETH), oe, xmrmaker.offerManager,
common.MoneroAmount(33), desiredAmount)
require.NoError(t, err)
swapState.SetContract(xmrmaker.backend.Contract())
swapState.SetContractAddress(xmrmaker.backend.ContractAddr())
return xmrmaker, swapState, db
}
@@ -311,7 +314,7 @@ func TestSwapState_HandleProtocolMessage_NotifyETHLocked_timeout(t *testing.T) {
require.Equal(t, duration, s.t1.Sub(s.t0))
require.Equal(t, &message.NotifyReady{}, s.nextExpectedMessage)
for status := range s.statusCh {
for status := range s.offerExtra.StatusCh {
if status == types.CompletedSuccess {
break
} else if !status.IsOngoing() {
@@ -525,7 +528,7 @@ func TestSwapState_Exit_Refunded(t *testing.T) {
s.offer = types.NewOffer(types.ProvidesXMR, 0.1, 0.2, 0.1, types.EthAssetETH)
db.EXPECT().PutOffer(s.offer)
b.MakeOffer(s.offer)
b.MakeOffer(s.offer, "", 0)
s.info.SetStatus(types.CompletedRefund)
err := s.Exit()

View File

@@ -1,30 +1,10 @@
package xmrmaker
import (
"bytes"
"context"
contracts "github.com/athanorlabs/atomic-swap/ethereum"
"github.com/athanorlabs/atomic-swap/net/message"
"github.com/athanorlabs/atomic-swap/protocol/backend"
ethcommon "github.com/ethereum/go-ethereum/common"
)
func checkContractCode(ctx context.Context, b backend.Backend, contractAddr ethcommon.Address) error {
code, err := b.CodeAt(ctx, contractAddr, nil)
if err != nil {
return err
}
expectedCode := ethcommon.FromHex(contracts.SwapFactoryMetaData.Bin)
if !bytes.Contains(expectedCode, code) {
return errInvalidSwapContract
}
return nil
}
func convertContractSwap(msg *message.ContractSwap) contracts.SwapFactorySwap {
return contracts.SwapFactorySwap{
Owner: msg.Owner,

View File

@@ -10,6 +10,7 @@ import (
"time"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
logging "github.com/ipfs/go-log"
"github.com/stretchr/testify/require"
@@ -56,7 +57,8 @@ func newBackend(t *testing.T) backend.Backend {
txOpts, err := bind.NewKeyedTransactorWithChainID(pk, chainID)
require.NoError(t, err)
_, tx, contract, err := contracts.DeploySwapFactory(txOpts, ec)
var forwarderAddress ethcommon.Address
_, tx, contract, err := contracts.DeploySwapFactory(txOpts, ec, forwarderAddress)
require.NoError(t, err)
addr, err := bind.WaitDeployed(ctx, ec, tx)
@@ -79,32 +81,6 @@ func newBackend(t *testing.T) backend.Backend {
return b
}
func newXMRMakerBackend(t *testing.T) backend.Backend {
pk := tests.GetMakerTestKey(t)
ec, chainID := tests.NewEthClient(t)
txOpts, err := bind.NewKeyedTransactorWithChainID(pk, chainID)
require.NoError(t, err)
addr, _, contract, err := contracts.DeploySwapFactory(txOpts, ec)
require.NoError(t, err)
bcfg := &backend.Config{
Ctx: context.Background(),
MoneroClient: monero.CreateWalletClient(t),
EthereumClient: ec,
EthereumPrivateKey: pk,
Environment: common.Development,
SwapManager: pswap.NewManager(),
SwapContract: contract,
SwapContractAddress: addr,
Net: new(mockNet),
}
b, err := backend.NewBackend(bcfg)
require.NoError(t, err)
return b
}
func newTestInstance(t *testing.T) *swapState {
b := newBackend(t)
swapState, err := newSwapState(b, types.Hash{}, infofile, false,
@@ -306,11 +282,11 @@ func TestSwapState_NotifyClaimed(t *testing.T) {
s.SetSwapTimeout(time.Minute * 2)
// close swap-deposit-wallet
maker := newXMRMakerBackend(t)
err := maker.CreateWallet("test-wallet", "")
backend := newBackend(t)
err := backend.CreateWallet("test-wallet", "")
require.NoError(t, err)
monero.MineMinXMRBalance(t, maker, common.MoneroToPiconero(1))
monero.MineMinXMRBalance(t, backend, common.MoneroToPiconero(1))
// invalid SendKeysMessage should result in an error
msg := &net.SendKeysMessage{}
@@ -340,7 +316,7 @@ func TestSwapState_NotifyClaimed(t *testing.T) {
xmrAddr := kp.Address(common.Mainnet)
// lock xmr
tResp, err := maker.Transfer(xmrAddr, 0, uint64(amt))
tResp, err := backend.Transfer(xmrAddr, 0, uint64(amt))
require.NoError(t, err)
t.Logf("transferred %d pico XMR (fees %d) to account %s", tResp.Amount, tResp.Fee, xmrAddr)
require.Equal(t, uint64(amt), tResp.Amount)