From 1d030df189f4d313c67352eef32864584fe8ab29 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Fri, 5 Mar 2021 17:27:04 +0900 Subject: [PATCH] multi: Add ETHSwapV0. Have the eth harness start with a contract. Add contract bindings and basic calls to clients. --- client/asset/eth/eth.go | 5 + client/asset/eth/eth_test.go | 14 + client/asset/eth/rpcclient.go | 65 +++- client/asset/eth/rpcclient_harness_test.go | 392 ++++++++++++++++++++- dex/testing/eth/harness.sh | 25 ++ server/asset/eth/common.go | 31 +- server/asset/eth/contract.go | 379 ++++++++++++++++++++ server/asset/eth/contracts/ETHSwapV0.sol | 84 +++++ server/asset/eth/contracts/README.md | 13 + 9 files changed, 990 insertions(+), 18 deletions(-) create mode 100644 server/asset/eth/contract.go create mode 100644 server/asset/eth/contracts/ETHSwapV0.sol create mode 100644 server/asset/eth/contracts/README.md diff --git a/client/asset/eth/eth.go b/client/asset/eth/eth.go index f14abf8ee5..fe83eb4099 100644 --- a/client/asset/eth/eth.go +++ b/client/asset/eth/eth.go @@ -19,6 +19,7 @@ import ( "decred.org/dcrdex/server/asset/eth" "github.com/decred/dcrd/dcrutil/v3" "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" @@ -107,13 +108,17 @@ type ethFetcher interface { blockNumber(ctx context.Context) (uint64, error) connect(ctx context.Context, node *node.Node, contractAddr common.Address) error importAccount(pw string, privKeyB []byte) (*accounts.Account, 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 locked(ctx context.Context, acct *accounts.Account) (bool, error) nodeInfo(ctx context.Context) (*p2p.NodeInfo, error) pendingTransactions(ctx context.Context) ([]*types.Transaction, error) transactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, 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) sendToAddr(ctx context.Context, acct *accounts.Account, addr common.Address, amt, gasFee *big.Int) (common.Hash, error) shutdown() + swap(ctx context.Context, from *accounts.Account, secretHash [32]byte) (*eth.ETHSwapSwap, error) syncStatus(ctx context.Context) (bool, float32, error) unlock(ctx context.Context, pw string, acct *accounts.Account) error } diff --git a/client/asset/eth/eth_test.go b/client/asset/eth/eth_test.go index 6aa12d3c56..f8a9db2d68 100644 --- a/client/asset/eth/eth_test.go +++ b/client/asset/eth/eth_test.go @@ -11,7 +11,9 @@ import ( "testing" "decred.org/dcrdex/dex" + "decred.org/dcrdex/server/asset/eth" "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" @@ -77,6 +79,18 @@ func (n *testNode) blockNumber(ctx context.Context) (uint64, error) { 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) (*eth.ETHSwapSwap, error) { + return nil, nil +} func (n *testNode) transactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { return nil, nil } diff --git a/client/asset/eth/rpcclient.go b/client/asset/eth/rpcclient.go index e727d58c54..6fba1aadff 100644 --- a/client/asset/eth/rpcclient.go +++ b/client/asset/eth/rpcclient.go @@ -10,7 +10,9 @@ import ( "fmt" "math/big" + "decred.org/dcrdex/server/asset/eth" "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" @@ -32,11 +34,12 @@ type rpcclient struct { // ec wraps the client with some useful calls. ec *ethclient.Client n *node.Node + es *eth.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) @@ -44,6 +47,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 = eth.NewETHSwap(contractAddr, c.ec) + if err != nil { + return fmt.Errorf("unable to find swap contract: %v", err) + } return nil } @@ -245,3 +252,59 @@ func (c *rpcclient) wallet(acct accounts.Account) (accounts.Wallet, error) { } return wallet, nil } + +// swap gets a swap keyed by secretHash in the contract. +func (c *rpcclient) swap(ctx context.Context, from *accounts.Account, secretHash [32]byte) (*eth.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 +} + +// 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) { + wallet, err := c.wallet(accounts.Account{Address: txOpts.From}) + if err != nil { + return nil, 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 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 mathing the hash, will not cause +// this to error. +func (c *rpcclient) redeem(txOpts *bind.TransactOpts, netID int64, secret, secretHash [32]byte) (*types.Transaction, error) { + wallet, err := c.wallet(accounts.Account{Address: txOpts.From}) + if err != nil { + return nil, 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 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) { + wallet, err := c.wallet(accounts.Account{Address: txOpts.From}) + if err != nil { + return nil, 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 c.es.Refund(txOpts, secretHash) +} diff --git a/client/asset/eth/rpcclient_harness_test.go b/client/asset/eth/rpcclient_harness_test.go index 7a411a3116..36c936abc8 100644 --- a/client/asset/eth/rpcclient_harness_test.go +++ b/client/asset/eth/rpcclient_harness_test.go @@ -16,6 +16,7 @@ package eth import ( "context" + "crypto/sha256" "encoding/hex" "errors" "fmt" @@ -29,6 +30,8 @@ import ( "decred.org/dcrdex/client/asset" "decred.org/dcrdex/dex" + "decred.org/dcrdex/dex/encode" + "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" @@ -41,19 +44,20 @@ const ( ) var ( - gasPrice = big.NewInt(82e9) - homeDir = os.Getenv("HOME") - genesisFile = filepath.Join(homeDir, "dextest", "eth", "genesis.json") - 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") + genesisFile = filepath.Join(homeDir, "dextest", "eth", "genesis.json") + 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 { return &bind.TransactOpts{ GasPrice: gasPrice, GasLimit: 1e6, @@ -108,6 +112,18 @@ func TestMain(m *testing.M) { os.Exit(1) } defer os.RemoveAll(tmpDir) + addrBytes, err := ioutil.ReadFile(contractAddrFile) + if err != nil { + fmt.Printf("error reading contract address: %v\n", err) + os.Exit(1) + } + addrLen := len(addrBytes) + if addrLen == 0 { + fmt.Printf("no contract address found at %v\n", contractAddrFile) + os.Exit(1) + } + addrBytes = addrBytes[:addrLen-1] + fmt.Printf("Contract address is %v\n", string(addrBytes)) genBytes, err := ioutil.ReadFile(genesisFile) if err != nil { fmt.Printf("error reading genesis file: %v\n", err) @@ -135,7 +151,7 @@ func TestMain(m *testing.M) { 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(string(addrBytes))); err != nil { fmt.Printf("connect error: %v\n", err) os.Exit(1) } @@ -310,6 +326,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 TestSyncStatus(t *testing.T) { synced, ratio, err := ethClient.syncStatus(ctx) if err != nil { @@ -318,3 +345,340 @@ func TestSyncStatus(t *testing.T) { spew.Dump(synced) spew.Dump(ratio) } + +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("unexpeted 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, simnetAcct) + if err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + + tx, err := ethClient.initiate(txOpts, simnetID, now, secretHash, participantAddr) + if err != nil { + t.Fatalf("unexpeted error for test %v: %v", test.name, err) + } + spew.Dump(tx) + + if err := waitForMined(t, time.Second*10, false); err != nil { + t.Fatalf("unexpeted 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("unexpeted 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 it subtracted. If successful, amt is + // also subtracted. + bal, err := ethClient.balance(ctx, simnetAcct) + if err != nil { + t.Fatalf("unexpeted 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("unexpeted 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("unexpeted error for test %v: %v", test.name, err) + } + state := eth.SwapState(swap.State) + if state != test.finalState { + t.Fatalf("unexpeted 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 secred", + 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("unexpeted swap state for test %v: want %s got %s", test.name, eth.None, state) + } + + // Create a secret that doesn't has to secredHash. + if test.badSecret { + copy(secret[:], encode.RandomBytes(32)) + } + + inLocktime := time.Now().Add(locktime).Unix() + _, err = ethClient.initiate(txOpts, simnetID, inLocktime, secretHash, participantAcct.Address) + 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("unexpeted error for test %v: %v", test.name, err) + } + originalBal, err := ethClient.balance(ctx, test.redeemer) + if err != nil { + t.Fatalf("unexpeted 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("unexpeted error for test %v: %v", test.name, err) + } + spew.Dump(tx) + + if err := waitForMined(t, time.Second*10, false); err != nil { + t.Fatalf("unexpeted 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("unexpeted 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 it subtracted. If successful, amt is + // added. + bal, err := ethClient.balance(ctx, test.redeemer) + if err != nil { + t.Fatalf("unexpeted 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("unexpeted 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("unexpeted error for test %v: %v", test.name, err) + } + state = eth.SwapState(swap.State) + if state != test.finalState { + t.Fatalf("unexpeted 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("unexpeted 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, participantAcct.Address) + 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("unexpeted 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("unexpeted 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("unexpeted error for test %v: %v", test.name, err) + } + + originalBal, err := ethClient.balance(ctx, test.refunder) + if err != nil { + t.Fatalf("unexpeted 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("unexpeted error for test %v: %v", test.name, err) + } + spew.Dump(tx) + + if err := waitForMined(t, time.Second*10, false); err != nil { + t.Fatalf("unexpeted 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("unexpeted 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 it subtracted. If successful, amt is + // added. + bal, err := ethClient.balance(ctx, test.refunder) + if err != nil { + t.Fatalf("unexpeted 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("unexpeted 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("unexpeted error for test %v: %v", test.name, err) + } + state = eth.SwapState(swap.State) + if state != test.finalState { + t.Fatalf("unexpeted swap state for test %v: want %s got %s", test.name, test.finalState, state) + } + } +} diff --git a/dex/testing/eth/harness.sh b/dex/testing/eth/harness.sh index dac558d4ff..32440f4617 100755 --- a/dex/testing/eth/harness.sh +++ b/dex/testing/eth/harness.sh @@ -36,6 +36,8 @@ DELTA_NODE_KEY="725394672587b34bbf15580c59e5199c75c2c7e998ba8df3cb38cc4347d46e2b DELTA_ENODE="ca414c361d1a38716170923e4900d9dc9203dbaf8fdcaee73e1f861df9fdf20a1453b76fd218c18bc6f3c7e13cbca0b3416af02a53b8e31188faa45aab398d1c" DELTA_NODE_PORT="30307" +ETH_SWAP_V0="608060405234801561001057600080fd5b50610758806100206000396000f3fe60806040526004361061004a5760003560e01c80637249fbb61461004f57806376467cbd14610071578063ae052147146100a7578063b31597ad146100ba578063eb84e7f2146100da575b600080fd5b34801561005b57600080fd5b5061006f61006a366004610565565b61010e565b005b34801561007d57600080fd5b5061009161008c366004610565565b6101f7565b60405161009e919061065d565b60405180910390f35b61006f6100b53660046105b6565b6102c1565b3480156100c657600080fd5b5061006f6100d5366004610595565b610388565b3480156100e657600080fd5b506100fa6100f5366004610565565b6104d0565b60405161009e9897969594939291906106cd565b8033600160008381526020819052604090206007015460ff16600381111561014657634e487b7160e01b600052602160045260246000fd5b1461015057600080fd5b6000828152602081905260409020600401546001600160a01b0382811691161461017957600080fd5b6000828152602081905260409020600101544281111561019857600080fd5b600084815260208190526040808220600601549051339282156108fc02929190818181858888f193505050501580156101d5573d6000803e3d6000fd5b505050600091825250602081905260409020600701805460ff19166003179055565b6101ff610523565b6000828152602081815260409182902082516101008101845281548152600182015492810192909252600281015492820192909252600380830154606083015260048301546001600160a01b03908116608084015260058401541660a0830152600683015460c0830152600783015491929160e084019160ff9091169081111561029957634e487b7160e01b600052602160045260246000fd5b60038111156102b857634e487b7160e01b600052602160045260246000fd5b90525092915050565b82600034116102cf57600080fd5b600081116102dc57600080fd5b826000808281526020819052604090206007015460ff16600381111561031257634e487b7160e01b600052602160045260246000fd5b1461031c57600080fd5b6000848152602081905260409020438155600180820187905560028201869055600482018054336001600160a01b0319918216179091556005830180549091166001600160a01b0387161790553460068301556007909101805460ff1916828002179055505050505050565b808233600160008481526020819052604090206007015460ff1660038111156103c157634e487b7160e01b600052602160045260246000fd5b146103cb57600080fd5b6000838152602081905260409020600501546001600160a01b038281169116146103f457600080fd5b82600283604051602001610408919061061b565b60408051601f198184030181529082905261042291610624565b602060405180830381855afa15801561043f573d6000803e3d6000fd5b5050506040513d601f19601f82011682018060405250810190610462919061057d565b1461046c57600080fd5b600084815260208190526040808220600601549051339282156108fc02929190818181858888f193505050501580156104a9573d6000803e3d6000fd5b50505060009182525060208190526040902060078101805460ff1916600217905560030155565b6000602081905290815260409020805460018201546002830154600384015460048501546005860154600687015460079097015495969495939492936001600160a01b0392831693919092169160ff1688565b6040805161010081018252600080825260208201819052918101829052606081018290526080810182905260a0810182905260c081018290529060e082015290565b600060208284031215610576578081fd5b5035919050565b60006020828403121561058e578081fd5b5051919050565b600080604083850312156105a7578081fd5b50508035926020909101359150565b6000806000606084860312156105ca578081fd5b833592506020840135915060408401356001600160a01b03811681146105ee578182fd5b809150509250925092565b6004811061061757634e487b7160e01b600052602160045260246000fd5b9052565b90815260200190565b60008251815b81811015610644576020818601810151858301520161062a565b818111156106525782828501525b509190910192915050565b60006101008201905082518252602083015160208301526040830151604083015260608301516060830152608083015160018060a01b0380821660808501528060a08601511660a0850152505060c083015160c083015260e08301516106c660e08401826105f9565b5092915050565b8881526020810188905260408101879052606081018690526001600160a01b038581166080830152841660a082015260c08101839052610100810161071560e08301846105f9565b999850505050505050505056fea26469706673582212204ae7685f717de45f989d837f9275de6e36bef2be566fd8950bde0a539301c70064736f6c63430008010033" + # PASSWORD is the password used to unlock all accounts/wallets/addresses. PASSWORD="abc" @@ -104,6 +106,20 @@ function send(from, to, amt) { } EOF +cat > "${NODES_ROOT}/harness-ctl/deploy.js" < "${NODES_ROOT}/harness-ctl/contractAddress.js" < "${NODES_ROOT}/contract_addr.txt" <=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); + _; + } + + 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 + isRedeemable(secretHash, secret, msg.sender) + { + payable(msg.sender).transfer(swaps[secretHash].value); + swaps[secretHash].state = State.Redeemed; + swaps[secretHash].secret = secret; + } + + function refund(bytes32 secretHash) + public + isRefundable(secretHash, msg.sender) + { + payable(msg.sender).transfer(swaps[secretHash].value); + swaps[secretHash].state = State.Refunded; + } +} diff --git a/server/asset/eth/contracts/README.md b/server/asset/eth/contracts/README.md new file mode 100644 index 0000000000..25882d57e3 --- /dev/null +++ b/server/asset/eth/contracts/README.md @@ -0,0 +1,13 @@ +## 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 + +mainnet addres is `to be determined` + +ETHSwapV0.sol is the first interation of the the eth swap smart contract.