Skip to content

Commit

Permalink
eth/client: Implement MaxOrder
Browse files Browse the repository at this point in the history
This completes the MaxOrder function for ETH and also adds
functions that calculate the gas needed for initializing and
redeeming a swap on ETH. The gas calculation is function is used
in MaxOrder, but in case of a calculation failure, the hard-coded
gas value that is returned from the server is used.
  • Loading branch information
martonp committed Sep 29, 2021
1 parent 59c693a commit 72212a8
Show file tree
Hide file tree
Showing 4 changed files with 293 additions and 2 deletions.
59 changes: 57 additions & 2 deletions client/asset/eth/eth.go
Expand Up @@ -21,11 +21,13 @@ import (

"decred.org/dcrdex/client/asset"
"decred.org/dcrdex/dex"
"decred.org/dcrdex/dex/encode"
swap "decred.org/dcrdex/dex/networks/eth"
dexeth "decred.org/dcrdex/server/asset/eth"
"github.com/decred/dcrd/dcrutil/v4"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
Expand All @@ -42,6 +44,8 @@ const (
BipID = 60
defaultGasFee = 82 // gwei
defaultGasFeeLimit = 200 // gwei

RedeemGas = 63000 // gas
)

var (
Expand Down Expand Up @@ -120,6 +124,7 @@ type ethFetcher interface {
importAccount(pw string, privKeyB []byte) (*accounts.Account, error)
listWallets(ctx context.Context) ([]rawWallet, error)
initiate(opts *bind.TransactOpts, netID int64, refundTimestamp int64, secretHash [32]byte, participant *common.Address) (*types.Transaction, error)
estimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error)
lock(ctx context.Context, acct *accounts.Account) error
nodeInfo(ctx context.Context) (*p2p.NodeInfo, error)
pendingTransactions(ctx context.Context) ([]*types.Transaction, error)
Expand Down Expand Up @@ -295,13 +300,63 @@ func (eth *ExchangeWallet) Balance() (*asset.Balance, error) {
return bal, nil
}

// getInitGas gets an estimate for the gas required for initiating a swap.
func (eth *ExchangeWallet) getInitGas() (uint64, error) {
var secretHash [32]byte
copy(secretHash[:], encode.RandomBytes(32))
parsedAbi, err := abi.JSON(strings.NewReader(swap.ETHSwapABI))
if err != nil {
return 0, err
}
data, err := parsedAbi.Pack("initiate", big.NewInt(1), secretHash, &eth.acct.Address)
if err != nil {
return 0, err
}
msg := ethereum.CallMsg{
From: eth.acct.Address,
To: &mainnetContractAddr,
Value: big.NewInt(1),
Gas: 0,
Data: data,
}
return eth.node.estimateGas(eth.ctx, msg)
}

// MaxOrder generates information about the maximum order size and associated
// fees that the wallet can support for the given DEX configuration. The fees are an
// estimate based on current network conditions, and will be <= the fees
// associated with nfo.MaxFeeRate. For quote assets, the caller will have to
// calculate lotSize based on a rate conversion from the base asset's lot size.
func (*ExchangeWallet) MaxOrder(lotSize uint64, feeSuggestion uint64, nfo *dex.Asset) (*asset.SwapEstimate, error) {
return nil, asset.ErrNotImplemented
func (eth *ExchangeWallet) MaxOrder(lotSize uint64, feeSuggestion uint64, nfo *dex.Asset) (*asset.SwapEstimate, error) {
balance, err := eth.Balance()
if err != nil {
return nil, err
}
initGas, err := eth.getInitGas()
if err != nil {
eth.log.Warnf("error getting init gas, falling back to server's value: %v", err)
initGas = nfo.SwapSize
}
availableBalance := balance.Available
maxTxFee := initGas * nfo.MaxFeeRate
realisticTxFee := initGas * feeSuggestion
lots := availableBalance / (lotSize + maxTxFee)
if lots < 1 {
return &asset.SwapEstimate{}, nil
}
value := lots * lotSize
maxFees := lots * maxTxFee
realisticWorstCase := realisticTxFee * lots
realisticBestCase := realisticTxFee
locked := value + maxFees
return &asset.SwapEstimate{
Lots: lots,
Value: value,
MaxFees: maxFees,
RealisticWorstCase: realisticWorstCase,
RealisticBestCase: realisticBestCase,
Locked: locked,
}, nil
}

// PreSwap gets order estimates based on the available funds and the wallet
Expand Down
154 changes: 154 additions & 0 deletions client/asset/eth/eth_test.go
Expand Up @@ -45,6 +45,8 @@ type testNode struct {
peersErr error
bal *big.Int
balErr error
initGas uint64
initGasErr error
}

func (n *testNode) connect(ctx context.Context, node *node.Node, addr *common.Address) error {
Expand Down Expand Up @@ -117,6 +119,9 @@ func (n *testNode) transactionReceipt(ctx context.Context, txHash common.Hash) (
func (n *testNode) peers(ctx context.Context) ([]*p2p.PeerInfo, error) {
return n.peerInfo, n.peersErr
}
func (n *testNode) estimateGas(ctx context.Context, callMsg ethereum.CallMsg) (uint64, error) {
return n.initGas, n.initGasErr
}

func TestLoadConfig(t *testing.T) {
tests := []struct {
Expand Down Expand Up @@ -349,3 +354,152 @@ func TestBalance(t *testing.T) {
}
}
}

func TestMaxOrder(t *testing.T) {
ethToGwei := func(eth uint64) uint64 {
return eth * dexeth.GweiFactor
}

ethToWei := func(eth int64) *big.Int {
return big.NewInt(0).Mul(big.NewInt(eth*dexeth.GweiFactor), big.NewInt(dexeth.GweiFactor))
}

estimatedInitGas := uint64(180000)
hardcodedInitGas := uint64(170000)
tests := []struct {
name string
bal *big.Int
balErr error
initGasErr error
lotSize uint64
maxFeeRate uint64
feeSuggestion uint64
wantErr bool
wantLots uint64
wantValue uint64
wantMaxFees uint64
wantWorstCase uint64
wantBestCase uint64
wantLocked uint64
}{
{
name: "no balance",
bal: big.NewInt(0),
lotSize: ethToGwei(10),
feeSuggestion: 90,
maxFeeRate: 100,
},
{
name: "not enough for fees",
bal: ethToWei(10),
lotSize: ethToGwei(10),
feeSuggestion: 90,
maxFeeRate: 100,
},
{
name: "one lot enough for fees",
bal: ethToWei(11),
lotSize: ethToGwei(10),
feeSuggestion: 90,
maxFeeRate: 100,
wantLots: 1,
wantValue: ethToGwei(10),
wantMaxFees: 100 * estimatedInitGas,
wantBestCase: 90 * estimatedInitGas,
wantWorstCase: 90 * estimatedInitGas,
wantLocked: ethToGwei(10) + (100 * estimatedInitGas),
},
{
name: "multiple lots",
bal: ethToWei(51),
lotSize: ethToGwei(10),
feeSuggestion: 90,
maxFeeRate: 100,
wantLots: 5,
wantValue: ethToGwei(50),
wantMaxFees: 5 * 100 * estimatedInitGas,
wantBestCase: 90 * estimatedInitGas,
wantWorstCase: 5 * 90 * estimatedInitGas,
wantLocked: ethToGwei(50) + (5 * 100 * estimatedInitGas),
},
{
name: "balanceError",
bal: ethToWei(51),
lotSize: ethToGwei(10),
feeSuggestion: 90,
maxFeeRate: 100,
balErr: errors.New(""),
wantErr: true,
},
{
name: "initGasError",
bal: ethToWei(51),
lotSize: ethToGwei(10),
feeSuggestion: 90,
maxFeeRate: 100,
initGasErr: errors.New(""),
wantLots: 5,
wantValue: ethToGwei(50),
wantMaxFees: 5 * 100 * hardcodedInitGas,
wantBestCase: 90 * hardcodedInitGas,
wantWorstCase: 5 * 90 * hardcodedInitGas,
wantLocked: ethToGwei(50) + (5 * 100 * hardcodedInitGas),
},
}

dexAsset := dex.Asset{
ID: 60,
Symbol: "ETH",
MaxFeeRate: 100,
SwapSize: hardcodedInitGas,
SwapSizeBase: 0,
SwapConf: 1,
}

for _, test := range tests {
ctx, cancel := context.WithCancel(context.Background())
node := &testNode{}
node.bal = test.bal
node.balErr = test.balErr
node.initGasErr = test.initGasErr
node.initGas = estimatedInitGas
eth := &ExchangeWallet{
node: node,
ctx: ctx,
log: tLogger,
acct: new(accounts.Account),
}
dexAsset.MaxFeeRate = test.maxFeeRate
maxOrder, err := eth.MaxOrder(test.lotSize, test.feeSuggestion, &dexAsset)
cancel()

if test.wantErr {
if err == nil {
t.Fatalf("expected error for test %q", test.name)
}
continue
}
if err != nil {
t.Fatalf("unexpected error for test %q: %v", test.name, err)
}

if maxOrder.Lots != test.wantLots {
t.Fatalf("want lots %v got %v for test %q", test.wantLots, maxOrder.Lots, test.name)
}
if maxOrder.Value != test.wantValue {
t.Fatalf("want value %v got %v for test %q", test.wantValue, maxOrder.Value, test.name)
}
if maxOrder.MaxFees != test.wantMaxFees {
t.Fatalf("want maxFees %v got %v for test %q", test.wantMaxFees, maxOrder.MaxFees, test.name)
}
if maxOrder.RealisticBestCase != test.wantBestCase {
t.Fatalf("want best case %v got %v for test %q", test.wantBestCase, maxOrder.RealisticBestCase, test.name)
}
if maxOrder.RealisticWorstCase != test.wantWorstCase {
t.Fatalf("want worst case %v got %v for test %q", test.wantWorstCase, maxOrder.RealisticWorstCase, test.name)
}
if maxOrder.Locked != test.wantLocked {
t.Fatalf("want locked %v got %v for test %q", test.wantLocked, maxOrder.Locked, test.name)
}
}
}
5 changes: 5 additions & 0 deletions client/asset/eth/rpcclient.go
Expand Up @@ -259,6 +259,11 @@ func (c *rpcclient) initiate(txOpts *bind.TransactOpts, netID int64, refundTimes
return c.es.Initiate(txOpts, big.NewInt(refundTimestamp), secretHash, *participant)
}

// estimateGas checks the amount of gas that is used for a function call.
func (c *rpcclient) estimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) {
return c.ec.EstimateGas(ctx, msg)
}

// redeem redeems a swap contract. The redeemer will be the account at txOpts.From.
// Any on-chain failure, such as this secret not matching the hash, will not cause
// this to error.
Expand Down
77 changes: 77 additions & 0 deletions client/asset/eth/rpcclient_harness_test.go
Expand Up @@ -33,17 +33,21 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"

"decred.org/dcrdex/client/asset"
"decred.org/dcrdex/dex"
"decred.org/dcrdex/dex/encode"
dexeth "decred.org/dcrdex/dex/networks/eth"
swap "decred.org/dcrdex/dex/networks/eth"
"decred.org/dcrdex/internal/eth/reentryattack"
"decred.org/dcrdex/server/asset/eth"
"github.com/davecgh/go-spew/spew"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
)
Expand Down Expand Up @@ -372,6 +376,37 @@ func TestPeers(t *testing.T) {
spew.Dump(peers)
}

func TestInitiateGas(t *testing.T) {
var secretHash [32]byte
copy(secretHash[:], encode.RandomBytes(32))
parsedAbi, err := abi.JSON(strings.NewReader(swap.ETHSwapABI))
if err != nil {
t.Fatalf("unexpected error parsing abi: %v", err)
}
data, err := parsedAbi.Pack("initiate", big.NewInt(1), secretHash, &participantAddr)
if err != nil {
t.Fatalf("unexpected error packing abi: %v", err)
}
msg := ethereum.CallMsg{
From: participantAddr,
To: &contractAddr,
Value: big.NewInt(1),
Gas: 0,
Data: data,
}
gas, err := ethClient.estimateGas(ctx, msg)
if err != nil {
t.Fatalf("unexpected error from estimateGas: %v", err)
}
if gas > eth.InitGas {
t.Fatalf("actual gas %v is greater than eth.InitGas %v", gas, eth.InitGas)
}
if gas+10000 < eth.InitGas {
t.Fatalf("actual gas %v is much less than eth.InitGas %v", gas, eth.InitGas)
}
fmt.Printf("Gas used for initiate: %v \n", gas)
}

func TestInitiate(t *testing.T) {
now := time.Now().Unix()
var secretHash [32]byte
Expand Down Expand Up @@ -458,6 +493,48 @@ func TestInitiate(t *testing.T) {
}
}

func TestRedeemGas(t *testing.T) {
now := time.Now().Unix()
amt := big.NewInt(1e18)
txOpts := newTxOpts(ctx, &simnetAddr, amt)
var secret [32]byte
copy(secret[:], encode.RandomBytes(32))
secretHash := sha256.Sum256(secret[:])
_, err := ethClient.initiate(txOpts, simnetID, now, secretHash, &participantAddr)
if err != nil {
t.Fatalf("Unable to initiate swap: %v ", err)
}
if err := waitForMined(t, time.Second*8, true); err != nil {
t.Fatalf("unexpected error while waiting to mine: %v", err)
}
parsedAbi, err := abi.JSON(strings.NewReader(swap.ETHSwapABI))
if err != nil {
t.Fatalf("unexpected error parsing abi: %v", err)
}

data, err := parsedAbi.Pack("redeem", secret, secretHash)
if err != nil {
t.Fatalf("unexpected error packing abi: %v", err)
}
msg := ethereum.CallMsg{
From: participantAddr,
To: &contractAddr,
Gas: 0,
Data: data,
}
gas, err := ethClient.estimateGas(ctx, msg)
if err != nil {
t.Fatalf("Error estimating gas for redeem function: %v", err)
}
if gas > RedeemGas {
t.Fatalf("actual gas %v is greater than RedeemGas %v", gas, RedeemGas)
}
if gas+3000 < RedeemGas {
t.Fatalf("actual gas %v is much less than RedeemGas %v", gas, RedeemGas)
}
fmt.Printf("Gas used for redeem: %v \n", gas)
}

func TestRedeem(t *testing.T) {
amt := big.NewInt(1e18)
locktime := time.Second * 12
Expand Down

0 comments on commit 72212a8

Please sign in to comment.