implement SwapFactory.sol and integrate into codebase (#85)

This commit is contained in:
noot
2022-01-26 19:27:29 -05:00
committed by GitHub
parent b0b6cb5286
commit d62765a1ff
27 changed files with 1837 additions and 548 deletions

61
cmd/daemon/contract.go Normal file
View File

@@ -0,0 +1,61 @@
package main
import (
"crypto/ecdsa"
"fmt"
"math/big"
"github.com/noot/atomic-swap/common"
"github.com/noot/atomic-swap/swapfactory"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)
func getOrDeploySwapFactory(address ethcommon.Address, env common.Environment, basepath string, chainID *big.Int,
privkey *ecdsa.PrivateKey, ec *ethclient.Client) (*swapfactory.SwapFactory, ethcommon.Address, error) {
var (
sf *swapfactory.SwapFactory
)
if env == common.Development && (address == ethcommon.Address{}) {
txOpts, err := bind.NewKeyedTransactorWithChainID(privkey, chainID)
if err != nil {
return nil, ethcommon.Address{}, err
}
// deploy SwapFactory.sol
var tx *ethtypes.Transaction
address, tx, sf, err = deploySwapFactory(ec, txOpts)
if err != nil {
return nil, ethcommon.Address{}, err
}
log.Infof("deployed SwapFactory.sol: address=%s tx hash=%s", address, tx.Hash())
// store the contract address on disk
fp := fmt.Sprintf("%s/contractaddress", basepath)
if err = common.WriteContractAddressToFile(fp, address.String()); err != nil {
return nil, ethcommon.Address{}, fmt.Errorf("failed to write contract address to file: %w", err)
}
} else {
var err error
sf, err = getSwapFactory(ec, address)
if err != nil {
return nil, ethcommon.Address{}, err
}
log.Infof("loaded SwapFactory.sol from address %s", address)
}
return sf, address, nil
}
func getSwapFactory(client *ethclient.Client, addr ethcommon.Address) (*swapfactory.SwapFactory, error) {
return swapfactory.NewSwapFactory(addr, client)
}
func deploySwapFactory(client *ethclient.Client, txOpts *bind.TransactOpts) (ethcommon.Address, *ethtypes.Transaction, *swapfactory.SwapFactory, error) { //nolint:lll
return swapfactory.DeploySwapFactory(txOpts, client)
}

View File

@@ -6,6 +6,9 @@ import (
"os"
"strings"
ethcommon "github.com/ethereum/go-ethereum/common"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/urfave/cli"
"github.com/noot/atomic-swap/cmd/utils"
@@ -15,6 +18,7 @@ import (
"github.com/noot/atomic-swap/protocol/bob"
"github.com/noot/atomic-swap/protocol/swap"
"github.com/noot/atomic-swap/rpc"
"github.com/noot/atomic-swap/swapfactory"
logging "github.com/ipfs/go-log"
)
@@ -100,6 +104,10 @@ var (
Name: "ethereum-chain-id",
Usage: "ethereum chain ID; eg. mainnet=1, ropsten=3, rinkeby=4, goerli=5, ganache=1337",
},
&cli.StringFlag{
Name: "contract-address",
Usage: "address of instance of SwapFactory.sol already deployed on-chain",
},
&cli.StringFlag{
Name: "bootnodes",
Usage: "comma-separated string of libp2p bootnodes",
@@ -259,7 +267,7 @@ func runDaemon(c *cli.Context) error {
}
}()
log.Info("started swapd with basepath %d",
log.Infof("started swapd with basepath %d",
cfg.Basepath,
)
wait(ctx)
@@ -303,17 +311,46 @@ func getProtocolInstances(ctx context.Context, c *cli.Context, env common.Enviro
gasPrice = big.NewInt(int64(c.Uint("gas-price")))
}
var contractAddr ethcommon.Address
contractAddrStr := c.String("contract-address")
if contractAddrStr == "" {
contractAddr = ethcommon.Address{}
} else {
contractAddr = ethcommon.HexToAddress(contractAddrStr)
}
pk, err := ethcrypto.HexToECDSA(ethPrivKey)
if err != nil {
return nil, nil, err
}
ec, err := ethclient.Dial(ethEndpoint)
if err != nil {
return nil, nil, err
}
var contract *swapfactory.SwapFactory
if !devBob {
contract, contractAddr, err = getOrDeploySwapFactory(contractAddr, env, cfg.Basepath,
big.NewInt(chainID), pk, ec)
if err != nil {
return nil, nil, err
}
}
aliceCfg := &alice.Config{
Ctx: ctx,
Basepath: cfg.Basepath,
MoneroWalletEndpoint: moneroEndpoint,
EthereumEndpoint: ethEndpoint,
EthereumPrivateKey: ethPrivKey,
EthereumClient: ec,
EthereumPrivateKey: pk,
Environment: env,
ChainID: chainID,
ChainID: big.NewInt(chainID),
GasPrice: gasPrice,
GasLimit: uint64(c.Uint("gas-limit")),
SwapManager: sm,
SwapContract: contract,
SwapContractAddress: contractAddr,
}
a, err = alice.NewInstance(aliceCfg)
@@ -333,10 +370,10 @@ func getProtocolInstances(ctx context.Context, c *cli.Context, env common.Enviro
MoneroDaemonEndpoint: daemonEndpoint,
WalletFile: walletFile,
WalletPassword: walletPassword,
EthereumEndpoint: ethEndpoint,
EthereumPrivateKey: ethPrivKey,
EthereumClient: ec,
EthereumPrivateKey: pk,
Environment: env,
ChainID: chainID,
ChainID: big.NewInt(chainID),
GasPrice: gasPrice,
GasLimit: uint64(c.Uint("gas-limit")),
SwapManager: sm,
@@ -347,7 +384,7 @@ func getProtocolInstances(ctx context.Context, c *cli.Context, env common.Enviro
return nil, nil, err
}
log.Info("created swap protocol module with monero endpoint %s and ethereum endpoint %s",
log.Infof("created swap protocol module with monero endpoint %s and ethereum endpoint %s",
moneroEndpoint,
ethEndpoint,
)

View File

@@ -6,6 +6,8 @@ import (
"math/big"
"os"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/urfave/cli"
"github.com/noot/atomic-swap/cmd/utils"
@@ -32,7 +34,6 @@ var (
app = &cli.App{
Name: "swaprecover",
Usage: "A program for recovering swap funds due to unexpected shutdowns",
//Action: runRecover,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "env",
@@ -66,6 +67,10 @@ var (
Name: "gas-limit",
Usage: "ethereum gas limit to use for transactions. if not set, the gas limit is estimated for each transaction.",
},
&cli.UintFlag{
Name: "contract-swap-id",
Usage: "ID of the swap within the SwapFactory.sol contract",
},
},
Commands: []cli.Command{
{
@@ -102,8 +107,8 @@ func main() {
// MoneroRecoverer is implemented by a backend which is able to recover monero
type MoneroRecoverer interface {
WalletFromSecrets(aliceSecret, bobSecret string) (mcrypto.Address, error)
RecoverFromBobSecretAndContract(b *bob.Instance, bobSecret, contractAddr string) (*bob.RecoveryResult, error)
RecoverFromAliceSecretAndContract(a *alice.Instance, aliceSecret, contractAddr string) (*alice.RecoveryResult, error)
RecoverFromBobSecretAndContract(b *bob.Instance, bobSecret, contractAddr string, swapID *big.Int) (*bob.RecoveryResult, error) //nolint:lll
RecoverFromAliceSecretAndContract(a *alice.Instance, aliceSecret, contractAddr string, swapID *big.Int) (*alice.RecoveryResult, error) //nolint:lll
}
func runRecoverMonero(c *cli.Context) error {
@@ -128,6 +133,11 @@ func runRecoverMonero(c *cli.Context) error {
return errors.New("must also provide one of --contract-addr or --bob-secret")
}
swapID := big.NewInt(int64(c.Uint("contract-swap-id")))
if swapID.Uint64() == 0 {
log.Warn("provided contract swap ID of 0, this is likely not correct (unless you deployed the contract)")
}
r, err := getRecoverer(c, env)
if err != nil {
return err
@@ -149,7 +159,7 @@ func runRecoverMonero(c *cli.Context) error {
return err
}
res, err := r.RecoverFromBobSecretAndContract(b, bs, contractAddr)
res, err := r.RecoverFromBobSecretAndContract(b, bs, contractAddr, swapID)
if err != nil {
return err
}
@@ -171,7 +181,7 @@ func runRecoverMonero(c *cli.Context) error {
return err
}
res, err := r.RecoverFromAliceSecretAndContract(a, as, contractAddr)
res, err := r.RecoverFromAliceSecretAndContract(a, as, contractAddr, swapID)
if err != nil {
return err
}
@@ -249,14 +259,24 @@ func createAliceInstance(ctx context.Context, c *cli.Context, env common.Environ
gasPrice = big.NewInt(int64(c.Uint("gas-price")))
}
pk, err := ethcrypto.HexToECDSA(ethPrivKey)
if err != nil {
return nil, err
}
ec, err := ethclient.Dial(ethEndpoint)
if err != nil {
return nil, err
}
aliceCfg := &alice.Config{
Ctx: ctx,
Basepath: cfg.Basepath,
MoneroWalletEndpoint: moneroEndpoint,
EthereumEndpoint: ethEndpoint,
EthereumPrivateKey: ethPrivKey,
EthereumClient: ec,
EthereumPrivateKey: pk,
Environment: env,
ChainID: chainID,
ChainID: big.NewInt(chainID),
GasPrice: gasPrice,
GasLimit: uint64(c.Uint("gas-limit")),
}
@@ -298,14 +318,24 @@ func createBobInstance(ctx context.Context, c *cli.Context, env common.Environme
gasPrice = big.NewInt(int64(c.Uint("gas-price")))
}
pk, err := ethcrypto.HexToECDSA(ethPrivKey)
if err != nil {
return nil, err
}
ec, err := ethclient.Dial(ethEndpoint)
if err != nil {
return nil, err
}
bobCfg := &bob.Config{
Ctx: ctx,
Basepath: cfg.Basepath,
MoneroWalletEndpoint: moneroEndpoint,
EthereumEndpoint: ethEndpoint,
EthereumPrivateKey: ethPrivKey,
EthereumClient: ec,
EthereumPrivateKey: pk,
Environment: env,
ChainID: chainID,
ChainID: big.NewInt(chainID),
GasPrice: gasPrice,
GasLimit: uint64(c.Uint("gas-limit")),
}

View File

@@ -1,5 +1,35 @@
# Developing
## Deploying or using deployed SwapFactory.sol
The swap program uses a "factory" contract for the Ethereum side to reduce gas costs from deploying a new contract for each swap. The contract can be found in [here](../ethereum/contracts/SwapFactory.sol). For each new swap, the eth-holding party will call `NewSwap` on the factory contract, initiating a swap instance inside the contract.
If you're developing on a local network, running a `swapd` instance with the `--dev-alice` flag will automatically deploy an instance of `SwapFactory.sol` for you. You should see the following log shortly after starting `./swapd --dev-alice`:
```bash
# 2022-01-26T18:39:04.600-0500 INFO cmd daemon/contract.go:35 deployed SwapFactory.sol: address=0x3F2aF34E4250de94242Ac2B8A38550fd4503696d tx hash=0x638caf280178b3cfe06854b8a76a4ce355d38c5d81187836f0733cad1287b657
```
If you wish to use an instance of `SwapFactory.sol` that's already deployed on-chain, you can use the `--contract-address` flag to specify the address. For example:
```bash
$ ./swapd --dev-alice --contract-address 0x3F2aF34E4250de94242Ac2B8A38550fd4503696d
# 2022-01-26T18:56:31.627-0500 INFO cmd daemon/contract.go:42 loaded SwapFactory.sol from address 0x3F2aF34E4250de94242Ac2B8A38550fd4503696d
```
If you want to deploy the contract without running `swapd`, you can use hardhat. You will need node.js installed.
```bash
cd ethereum
npm install --save-dev hardhat
```
Then, you can run the deployment script against your desired network to deploy the contract.
```bash
$ npx hardhat run --network localhost scripts/deploy.js
# Compiling 2 files with 0.8.5
# Compilation finished successfully
# SwapFactory deployed to: 0xB0f6EC177Ab867E372479C0bfdBB0068bb5E1554
```
## Compiling DLEq binaries
The program utilizes a Rust DLEq library implemented by Farcaster.

View File

@@ -1,99 +0,0 @@
// SPDX-License-Identifier: LGPLv3
pragma solidity ^0.8.5;
import "./Secp256k1.sol";
contract Swap {
// Ed25519 library
Secp256k1 immutable secp256k1;
// contract creator, Alice
address payable immutable owner;
// address allowed to claim the ether in this contract
address payable immutable claimer;
// the keccak256 hash of the expected public key derived from the secret `s_b`.
// this public key is a point on the secp256k1 curve
bytes32 public immutable pubKeyClaim;
// the keccak256 hash of the expected public key derived from the secret `s_a`.
// this public key is a point on the secp256k1 curve
bytes32 public immutable pubKeyRefund;
// timestamp (set at contract creation)
// before which Alice can call either set_ready or refund
uint256 public immutable timeout_0;
// timestamp after which Bob cannot claim, only Alice can refund.
uint256 public immutable timeout_1;
// Alice sets ready to true when she sees the funds locked on the other chain.
// this prevents Bob from withdrawing funds without locking funds on the other chain first
bool public isReady = false;
event Constructed(bytes32 claimKey, bytes32 refundKey);
event Ready(bool b);
event Claimed(bytes32 s);
event Refunded(bytes32 s);
constructor(bytes32 _pubKeyClaim, bytes32 _pubKeyRefund, address payable _claimer, uint256 _timeoutDuration) payable {
owner = payable(msg.sender);
pubKeyClaim = _pubKeyClaim;
pubKeyRefund = _pubKeyRefund;
claimer = _claimer;
timeout_0 = block.timestamp + _timeoutDuration;
timeout_1 = block.timestamp + (_timeoutDuration * 2);
secp256k1 = new Secp256k1();
emit Constructed(_pubKeyClaim, _pubKeyRefund);
}
// Alice must call set_ready() within t_0 once she verifies the XMR has been locked
function set_ready() external {
require(!isReady && msg.sender == owner);
isReady = true;
emit Ready(true);
}
// Bob can claim if:
// - Alice doesn't call set_ready or refund within t_0, or
// - Alice calls ready within t_0, in which case Bob can call claim until t_1
function claim(bytes32 _s) external {
require(msg.sender == claimer, "only claimer can claim!");
require((block.timestamp >= timeout_0 || isReady), "too early to claim!");
require(block.timestamp < timeout_1, "too late to claim!");
verifySecret(_s, pubKeyClaim);
emit Claimed(_s);
// send eth to caller (Bob)
//selfdestruct(payable(msg.sender));
claimer.transfer(address(this).balance);
}
// Alice can claim a refund:
// - Until t_0 unless she calls set_ready
// - After t_1, if she called set_ready
function refund(bytes32 _s) external {
require(msg.sender == owner);
require(
block.timestamp >= timeout_1 || ( block.timestamp < timeout_0 && !isReady),
"It's Bob's turn now, please wait!"
);
verifySecret(_s, pubKeyRefund);
emit Refunded(_s);
// send eth back to owner==caller (Alice)
//selfdestruct(owner);
owner.transfer(address(this).balance);
}
function verifySecret(bytes32 _s, bytes32 pubKey) internal view {
require(
secp256k1.mulVerify(uint256(_s), uint256(pubKey)),
"provided secret does not match the expected pubKey"
);
}
}

View File

@@ -0,0 +1,133 @@
// SPDX-License-Identifier: LGPLv3
pragma solidity ^0.8.5;
import "./Secp256k1.sol";
contract SwapFactory {
Secp256k1 immutable secp256k1;
uint256 nextID;
struct Swap {
// contract creator, Alice
address payable owner;
// address allowed to claim the ether in this contract
address payable claimer;
// the keccak256 hash of the expected public key derived from the secret `s_b`.
// this public key is a point on the secp256k1 curve
bytes32 pubKeyClaim;
// the keccak256 hash of the expected public key derived from the secret `s_a`.
// this public key is a point on the secp256k1 curve
bytes32 pubKeyRefund;
// timestamp (set at contract creation)
// before which Alice can call either set_ready or refund
uint256 timeout_0;
// timestamp after which Bob cannot claim, only Alice can refund.
uint256 timeout_1;
// Alice sets ready to true when she sees the funds locked on the other chain.
// this prevents Bob from withdrawing funds without locking funds on the other chain first
bool isReady;
// set to true upon the swap value being claimed or refunded.
bool completed;
// the value of this swap.
uint256 value;
}
mapping(uint256 => Swap) public swaps;
event New(uint256 swapID, bytes32 claimKey, bytes32 refundKey);
event Ready(uint256 swapID);
event Claimed(uint256 swapID, bytes32 s);
event Refunded(uint256 swapID, bytes32 s);
constructor() {
secp256k1 = new Secp256k1();
}
// new_swap creates a new Swap instance with the given parameters.
// it returns the swap's ID.
function new_swap(bytes32 _pubKeyClaim,
bytes32 _pubKeyRefund,
address payable _claimer,
uint256 _timeoutDuration
) public payable returns (uint256) {
uint256 id = nextID;
Swap memory swap;
swap.owner = payable(msg.sender);
swap.claimer = _claimer;
swap.pubKeyClaim = _pubKeyClaim;
swap.pubKeyRefund = _pubKeyRefund;
swap.timeout_0 = block.timestamp + _timeoutDuration;
swap.timeout_1 = block.timestamp + (_timeoutDuration * 2);
swap.value = msg.value;
emit New(id, _pubKeyClaim, _pubKeyRefund);
nextID += 1;
swaps[id] = swap;
return id;
}
// Alice must call set_ready() within t_0 once she verifies the XMR has been locked
function set_ready(uint256 id) public {
require(!swaps[id].completed, "swap is already completed");
require(swaps[id].owner == msg.sender, "only the swap owner can call set_ready");
require(!swaps[id].isReady, "swap was already set to ready");
swaps[id].isReady = true;
emit Ready(id);
}
// Bob can claim if:
// - Alice doesn't call set_ready or refund within t_0, or
// - Alice calls ready within t_0, in which case Bob can call claim until t_1
function claim(uint256 id, bytes32 _s) public {
Swap memory swap = swaps[id];
require(!swap.completed, "swap is already completed");
require(msg.sender == swap.claimer, "only claimer can claim!");
require((block.timestamp >= swap.timeout_0 || swap.isReady), "too early to claim!");
require(block.timestamp < swap.timeout_1, "too late to claim!");
verifySecret(_s, swap.pubKeyClaim);
emit Claimed(id, _s);
// send eth to caller (Bob)
swap.claimer.transfer(swap.value);
swaps[id].completed = true;
}
// Alice can claim a refund:
// - Until t_0 unless she calls set_ready
// - After t_1, if she called set_ready
function refund(uint256 id, bytes32 _s) public {
Swap memory swap = swaps[id];
require(!swap.completed, "swap is already completed");
require(msg.sender == swap.owner, "refund must be called by the swap owner");
require(
block.timestamp >= swap.timeout_1 ||
(block.timestamp < swap.timeout_0 && !swap.isReady),
"it's the counterparty's turn, unable to refund, try again later"
);
verifySecret(_s, swap.pubKeyRefund);
emit Refunded(id, _s);
// send eth back to owner==caller (Alice)
swap.owner.transfer(swap.value);
swaps[id].completed = true;
}
function verifySecret(bytes32 _s, bytes32 pubKey) internal view {
require(
secp256k1.mulVerify(uint256(_s), uint256(pubKey)),
"provided secret does not match the expected public key"
);
}
}

View File

@@ -0,0 +1,13 @@
async function main() {
const SwapFactory = await ethers.getContractFactory("SwapFactory");
const contract = await SwapFactory.deploy();
console.log("SwapFactory deployed to:", contract.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"math/big"
"github.com/noot/atomic-swap/common/types"
)
@@ -168,12 +169,13 @@ func (m *SendKeysMessage) Type() MessageType {
// NotifyContractDeployed is sent by Alice to Bob after deploying the swap contract
// and locking her ether in it
type NotifyContractDeployed struct {
Address string
Address string
ContractSwapID *big.Int
}
// String ...
func (m *NotifyContractDeployed) String() string {
return "NotifyContractDeployed"
return fmt.Sprintf("NotifyContractDeployed Address=%s ContractSwapID=%d", m.Address, m.ContractSwapID)
}
// Encode ...

View File

@@ -7,6 +7,7 @@ import (
"sync"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
@@ -14,6 +15,7 @@ import (
"github.com/noot/atomic-swap/monero"
"github.com/noot/atomic-swap/net"
"github.com/noot/atomic-swap/protocol/swap"
"github.com/noot/atomic-swap/swapfactory"
logging "github.com/ipfs/go-log"
)
@@ -45,7 +47,9 @@ type Instance struct {
swapMu sync.Mutex
swapState *swapState
swapManager *swap.Manager
swapManager *swap.Manager
contract *swapfactory.SwapFactory
contractAddr ethcommon.Address
}
// Config contains the configuration values for a new Alice instance.
@@ -53,10 +57,12 @@ type Config struct {
Ctx context.Context
Basepath string
MoneroWalletEndpoint string
EthereumEndpoint string
EthereumPrivateKey string
EthereumClient *ethclient.Client
EthereumPrivateKey *ecdsa.PrivateKey
SwapContract *swapfactory.SwapFactory
SwapContractAddress ethcommon.Address
Environment common.Environment
ChainID int64
ChainID *big.Int
GasPrice *big.Int
GasLimit uint64
SwapManager *swap.Manager
@@ -66,32 +72,24 @@ type Config struct {
// It accepts an endpoint to a monero-wallet-rpc instance where Alice will generate
// the account in which the XMR will be deposited.
func NewInstance(cfg *Config) (*Instance, error) {
pk, err := crypto.HexToECDSA(cfg.EthereumPrivateKey)
if err != nil {
return nil, err
}
ec, err := ethclient.Dial(cfg.EthereumEndpoint)
if err != nil {
return nil, err
}
pub := pk.Public().(*ecdsa.PublicKey)
pub := cfg.EthereumPrivateKey.Public().(*ecdsa.PublicKey)
// TODO: check that Alice's monero-wallet-cli endpoint has wallet-dir configured
return &Instance{
ctx: cfg.Ctx,
basepath: cfg.Basepath,
env: cfg.Environment,
ethPrivKey: pk,
ethClient: ec,
ethPrivKey: cfg.EthereumPrivateKey,
ethClient: cfg.EthereumClient,
client: monero.NewClient(cfg.MoneroWalletEndpoint),
callOpts: &bind.CallOpts{
From: crypto.PubkeyToAddress(*pub),
Context: cfg.Ctx,
},
chainID: big.NewInt(cfg.ChainID),
swapManager: cfg.SwapManager,
chainID: cfg.ChainID,
swapManager: cfg.SwapManager,
contract: cfg.SwapContract,
contractAddr: cfg.SwapContractAddress,
}, nil
}

View File

@@ -11,7 +11,7 @@ import (
"github.com/noot/atomic-swap/net"
pcommon "github.com/noot/atomic-swap/protocol"
pswap "github.com/noot/atomic-swap/protocol/swap"
"github.com/noot/atomic-swap/swap-contract"
"github.com/noot/atomic-swap/swapfactory"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/fatih/color" //nolint:misspell
@@ -104,12 +104,12 @@ func (s *swapState) handleSendKeysMessage(msg *net.SendKeysMessage) (net.Message
}
s.setBobKeys(sk, vk, secp256k1Pub)
address, err := s.deployAndLockETH(s.providedAmountInWei())
err = s.lockETH(s.providedAmountInWei())
if err != nil {
return nil, fmt.Errorf("failed to deploy contract: %w", err)
}
log.Info("deployed Swap contract, waiting for XMR to be locked: contract address=", address)
log.Info("locked ether in swap contract, waiting for XMR to be locked")
// set t0 and t1
// TODO: these sometimes fail with "attempting to unmarshall an empty string while arguments are expected"
@@ -150,7 +150,8 @@ func (s *swapState) handleSendKeysMessage(msg *net.SendKeysMessage) (net.Message
s.nextExpectedMessage = &net.NotifyXMRLock{}
out := &net.NotifyContractDeployed{
Address: address.String(),
Address: s.alice.contractAddr.String(),
ContractSwapID: s.contractSwapID,
}
return out, nil
@@ -290,7 +291,7 @@ func (s *swapState) handleNotifyClaimed(txHash string) (mcrypto.Address, error)
return "", errors.New("claim transaction has no logs")
}
skB, err := swap.GetSecretFromLog(receipt.Logs[0], "Claimed")
skB, err := swapfactory.GetSecretFromLog(receipt.Logs[0], "Claimed")
if err != nil {
return "", fmt.Errorf("failed to get secret from log: %w", err)
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"math/big"
eth "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
@@ -11,10 +12,10 @@ import (
mcrypto "github.com/noot/atomic-swap/crypto/monero"
"github.com/noot/atomic-swap/dleq"
"github.com/noot/atomic-swap/swap-contract"
"github.com/noot/atomic-swap/swapfactory"
)
var claimedTopic = ethcommon.HexToHash("0xeddf608ef698454af2fb41c1df7b7e5154ff0d46969f895e0f39c7dfe7e6380a")
var claimedTopic = ethcommon.HexToHash("0xd5a2476fc450083bbb092dd3f4be92698ffdc2d213e6f1e730c7f44a52f1ccfc")
var (
errNoClaimLogsFound = errors.New("no Claimed logs found")
@@ -28,7 +29,7 @@ type recoveryState struct {
// NewRecoveryState returns a new *bob.recoveryState,
// which has methods to either claim ether or reclaim monero from an initiated swap.
func NewRecoveryState(a *Instance, secret *mcrypto.PrivateSpendKey,
contractAddr ethcommon.Address) (*recoveryState, error) { //nolint:revive
contractAddr ethcommon.Address, contractSwapID *big.Int) (*recoveryState, error) { //nolint:revive
txOpts, err := bind.NewKeyedTransactorWithChainID(a.ethPrivKey, a.chainID)
if err != nil {
return nil, err
@@ -49,13 +50,14 @@ func NewRecoveryState(a *Instance, secret *mcrypto.PrivateSpendKey,
ctx, cancel := context.WithCancel(a.ctx)
s := &swapState{
ctx: ctx,
cancel: cancel,
alice: a,
txOpts: txOpts,
privkeys: kp,
pubkeys: pubkp,
dleqProof: dleq.NewProofWithSecret(sc),
ctx: ctx,
cancel: cancel,
alice: a,
txOpts: txOpts,
privkeys: kp,
pubkeys: pubkp,
dleqProof: dleq.NewProofWithSecret(sc),
contractSwapID: contractSwapID,
}
rs := &recoveryState{
@@ -127,7 +129,7 @@ func (rs *recoveryState) ClaimOrRefund() (*RecoveryResult, error) {
func (rs *recoveryState) setContract(address ethcommon.Address) error {
var err error
rs.contractAddr = address
rs.ss.contract, err = swap.NewSwap(address, rs.ss.alice.ethClient)
rs.ss.alice.contract, err = swapfactory.NewSwapFactory(address, rs.ss.alice.ethClient)
return err
}
@@ -144,7 +146,7 @@ func (rs *recoveryState) filterForClaim() (*mcrypto.PrivateSpendKey, error) {
return nil, errNoClaimLogsFound
}
sa, err := swap.GetSecretFromLog(&logs[0], "Claimed")
sa, err := swapfactory.GetSecretFromLog(&logs[0], "Claimed")
if err != nil {
return nil, fmt.Errorf("failed to get secret from log: %w", err)
}

View File

@@ -21,10 +21,10 @@ func newTestRecoveryState(t *testing.T) *recoveryState {
s.setBobKeys(s.pubkeys.SpendKey(), s.privkeys.ViewKey(), akp.Secp256k1PublicKey)
s.bobAddress = inst.callOpts.From
addr, err := s.deployAndLockETH(common.NewEtherAmount(1))
err = s.lockETH(common.NewEtherAmount(1))
require.NoError(t, err)
rs, err := NewRecoveryState(inst, s.privkeys.SpendKey(), addr)
rs, err := NewRecoveryState(inst, s.privkeys.SpendKey(), inst.contractAddr, s.contractSwapID)
require.NoError(t, err)
return rs
}
@@ -40,7 +40,7 @@ func TestClaimOrRefund_Claim(t *testing.T) {
// call swap.Claim()
sc := rs.ss.getSecret()
_, err = rs.ss.contract.Claim(rs.ss.txOpts, sc)
_, err = rs.ss.alice.contract.Claim(rs.ss.txOpts, rs.ss.contractSwapID, sc)
require.NoError(t, err)
// assert we can claim the monero

View File

@@ -5,6 +5,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"math/big"
"sync"
"time"
@@ -16,7 +17,7 @@ import (
"github.com/noot/atomic-swap/net"
pcommon "github.com/noot/atomic-swap/protocol"
pswap "github.com/noot/atomic-swap/protocol/swap"
"github.com/noot/atomic-swap/swap-contract"
"github.com/noot/atomic-swap/swapfactory"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
@@ -51,9 +52,9 @@ type swapState struct {
bobAddress ethcommon.Address
// swap contract and timeouts in it; set once contract is deployed
contract *swap.Swap
t0, t1 time.Time
txOpts *bind.TransactOpts
contractSwapID *big.Int
t0, t1 time.Time
txOpts *bind.TransactOpts
// next expected network message
nextExpectedMessage net.Message // TODO: change to type?
@@ -196,7 +197,7 @@ func (s *swapState) tryRefund() (ethcommon.Hash, error) {
}
func (s *swapState) setTimeouts() error {
if s.contract == nil {
if s.alice.contract == nil {
return errors.New("contract is nil")
}
@@ -206,28 +207,16 @@ func (s *swapState) setTimeouts() error {
// TODO: add maxRetries
for {
log.Debug("attempting to fetch t0 from contract")
log.Debug("attempting to fetch timestamps from contract")
st0, err := s.contract.Timeout0(s.alice.callOpts)
info, err := s.alice.contract.Swaps(s.alice.callOpts, s.contractSwapID)
if err != nil {
time.Sleep(time.Second * 10)
continue
}
s.t0 = time.Unix(st0.Int64(), 0)
break
}
for {
log.Debug("attempting to fetch t1 from contract")
st1, err := s.contract.Timeout1(s.alice.callOpts)
if err != nil {
time.Sleep(time.Second * 10)
continue
}
s.t1 = time.Unix(st1.Int64(), 0)
s.t0 = time.Unix(info.Timeout0.Int64(), 0)
s.t1 = time.Unix(info.Timeout1.Int64(), 0)
break
}
@@ -279,14 +268,14 @@ func (s *swapState) setBobKeys(sk *mcrypto.PublicKey, vk *mcrypto.PrivateViewKey
s.bobSecp256k1PublicKey = secp256k1Pub
}
// deployAndLockETH deploys an instance of the Swap contract and locks `amount` ether in it.
func (s *swapState) deployAndLockETH(amount common.EtherAmount) (ethcommon.Address, error) {
// lockETH the Swap contract function new_swap and locks `amount` ether in it.
func (s *swapState) lockETH(amount common.EtherAmount) error {
if s.pubkeys == nil {
return ethcommon.Address{}, errors.New("public keys aren't set")
return errors.New("public keys aren't set")
}
if s.bobPublicSpendKey == nil || s.bobPrivateViewKey == nil {
return ethcommon.Address{}, errors.New("bob's keys aren't set")
return errors.New("bob's keys aren't set")
}
cmtAlice := s.secp256k1Pub.Keccak256()
@@ -297,38 +286,35 @@ func (s *swapState) deployAndLockETH(amount common.EtherAmount) (ethcommon.Addre
s.txOpts.Value = nil
}()
address, tx, swap, err := swap.DeploySwap(s.txOpts, s.alice.ethClient,
tx, err := s.alice.contract.NewSwap(s.txOpts,
cmtBob, cmtAlice, s.bobAddress, defaultTimeoutDuration)
if err != nil {
return ethcommon.Address{}, fmt.Errorf("failed to deploy Swap.sol: %w", err)
return fmt.Errorf("failed to deploy Swap.sol: %w", err)
}
log.Debugf("deploying Swap.sol, amount=%s txHash=%s", amount, tx.Hash())
if _, ok := common.WaitForReceipt(s.ctx, s.alice.ethClient, tx.Hash()); !ok {
return ethcommon.Address{}, errors.New("failed to deploy Swap.sol")
receipt, ok := common.WaitForReceipt(s.ctx, s.alice.ethClient, tx.Hash())
if !ok {
return errors.New("failed to call new_swap in contract")
}
fp := fmt.Sprintf("%s/%d/contractaddress", s.alice.basepath, s.info.ID())
if err = common.WriteContractAddressToFile(fp, address.String()); err != nil {
return ethcommon.Address{}, fmt.Errorf("failed to write contract address to file: %w", err)
if len(receipt.Logs) == 0 {
return errors.New("expected 1 log, got 0")
}
balance, err := s.alice.ethClient.BalanceAt(s.ctx, address, nil)
s.contractSwapID, err = swapfactory.GetIDFromLog(receipt.Logs[0])
if err != nil {
return ethcommon.Address{}, err
return err
}
log.Debug("contract balance: ", balance)
s.contract = swap
return address, nil
return nil
}
// ready calls the Ready() method on the Swap contract, indicating to Bob he has until time t_1 to
// call Claim(). Ready() should only be called once Alice sees Bob lock his XMR.
// If time t_0 has passed, there is no point of calling Ready().
func (s *swapState) ready() error {
tx, err := s.contract.SetReady(s.txOpts)
tx, err := s.alice.contract.SetReady(s.txOpts, s.contractSwapID)
if err != nil {
return err
}
@@ -344,14 +330,14 @@ func (s *swapState) ready() error {
// and returns to her the ether in the contract.
// If time t_1 passes and Claim() has not been called, Alice should call Refund().
func (s *swapState) refund() (ethcommon.Hash, error) {
if s.contract == nil {
if s.alice.contract == nil {
return ethcommon.Hash{}, errors.New("contract is nil")
}
sc := s.getSecret()
log.Infof("attempting to call Refund()...")
tx, err := s.contract.Refund(s.txOpts, sc)
tx, err := s.alice.contract.Refund(s.txOpts, s.contractSwapID, sc)
if err != nil {
return ethcommon.Hash{}, err
}

View File

@@ -7,13 +7,17 @@ import (
"testing"
"time"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/noot/atomic-swap/common"
mcrypto "github.com/noot/atomic-swap/crypto/monero"
"github.com/noot/atomic-swap/monero"
"github.com/noot/atomic-swap/net"
pcommon "github.com/noot/atomic-swap/protocol"
pswap "github.com/noot/atomic-swap/protocol/swap"
"github.com/noot/atomic-swap/swapfactory"
logging "github.com/ipfs/go-log"
"github.com/stretchr/testify/require"
@@ -31,15 +35,28 @@ func (n *mockNet) SendSwapMessage(msg net.Message) error {
}
func newTestInstance(t *testing.T) (*Instance, *swapState) {
pk, err := ethcrypto.HexToECDSA(common.DefaultPrivKeyAlice)
require.NoError(t, err)
ec, err := ethclient.Dial(common.DefaultEthEndpoint)
require.NoError(t, err)
txOpts, err := bind.NewKeyedTransactorWithChainID(pk, big.NewInt(common.MainnetConfig.EthereumChainID))
require.NoError(t, err)
addr, _, contract, err := swapfactory.DeploySwapFactory(txOpts, ec)
require.NoError(t, err)
cfg := &Config{
Ctx: context.Background(),
Basepath: "/tmp/alice",
MoneroWalletEndpoint: common.DefaultAliceMoneroEndpoint,
EthereumEndpoint: common.DefaultEthEndpoint,
EthereumPrivateKey: common.DefaultPrivKeyAlice,
EthereumClient: ec,
EthereumPrivateKey: pk,
Environment: common.Development,
ChainID: common.MainnetConfig.EthereumChainID,
ChainID: big.NewInt(common.MainnetConfig.EthereumChainID),
SwapManager: pswap.NewManager(),
SwapContract: contract,
SwapContractAddress: addr,
}
alice, err := NewInstance(cfg)
@@ -115,18 +132,15 @@ func TestSwapState_HandleProtocolMessage_SendKeysMessage_Refund(t *testing.T) {
require.Equal(t, bobKeysAndProof.PublicKeyPair.SpendKey().Hex(), s.bobPublicSpendKey.Hex())
require.Equal(t, bobKeysAndProof.PrivateKeyPair.ViewKey().Hex(), s.bobPrivateViewKey.Hex())
cdMsg, ok := resp.(*net.NotifyContractDeployed)
require.True(t, ok)
// ensure we refund before t0
time.Sleep(time.Second * 15)
require.NotNil(t, s.alice.net.(*mockNet).msg)
require.Equal(t, net.NotifyRefundType, s.alice.net.(*mockNet).msg.Type())
// check balance of contract is 0
balance, err := s.alice.ethClient.BalanceAt(s.ctx, ethcommon.HexToAddress(cdMsg.Address), nil)
// check swap is marked completed
info, err := s.alice.contract.Swaps(s.alice.callOpts, s.contractSwapID)
require.NoError(t, err)
require.Equal(t, int64(0), balance.Int64())
require.True(t, info.Completed)
}
func TestSwapState_NotifyXMRLock(t *testing.T) {
@@ -143,7 +157,7 @@ func TestSwapState_NotifyXMRLock(t *testing.T) {
s.setBobKeys(bobKeysAndProof.PublicKeyPair.SpendKey(), bobKeysAndProof.PrivateKeyPair.ViewKey(),
bobKeysAndProof.Secp256k1PublicKey)
_, err = s.deployAndLockETH(common.NewEtherAmount(1))
err = s.lockETH(common.NewEtherAmount(1))
require.NoError(t, err)
s.info.SetReceivedAmount(0)
@@ -185,7 +199,7 @@ func TestSwapState_NotifyXMRLock_Refund(t *testing.T) {
s.setBobKeys(bobKeysAndProof.PublicKeyPair.SpendKey(), bobKeysAndProof.PrivateKeyPair.ViewKey(),
bobKeysAndProof.Secp256k1PublicKey)
contractAddr, err := s.deployAndLockETH(common.NewEtherAmount(1))
err = s.lockETH(common.NewEtherAmount(1))
require.NoError(t, err)
s.info.SetReceivedAmount(0)
@@ -210,7 +224,7 @@ func TestSwapState_NotifyXMRLock_Refund(t *testing.T) {
require.Equal(t, net.NotifyRefundType, s.alice.net.(*mockNet).msg.Type())
// check balance of contract is 0
balance, err := s.alice.ethClient.BalanceAt(s.ctx, contractAddr, nil)
balance, err := s.alice.ethClient.BalanceAt(s.ctx, s.alice.contractAddr, nil)
require.NoError(t, err)
require.Equal(t, uint64(0), balance.Uint64())
}
@@ -284,7 +298,7 @@ func TestSwapState_NotifyClaimed(t *testing.T) {
var sc [32]byte
copy(sc[:], common.Reverse(secret))
tx, err := s.contract.Claim(s.txOpts, sc)
tx, err := s.alice.contract.Claim(s.txOpts, s.contractSwapID, sc)
require.NoError(t, err)
cmsg := &net.NotifyClaimed{

View File

@@ -59,10 +59,10 @@ type Config struct {
MoneroWalletEndpoint string
MoneroDaemonEndpoint string // only needed for development
WalletFile, WalletPassword string
EthereumEndpoint string
EthereumPrivateKey string
EthereumClient *ethclient.Client
EthereumPrivateKey *ecdsa.PrivateKey
Environment common.Environment
ChainID int64
ChainID *big.Int
GasPrice *big.Int
SwapManager *swap.Manager
GasLimit uint64
@@ -75,17 +75,7 @@ func NewInstance(cfg *Config) (*Instance, error) {
return nil, errors.New("environment is development, must provide monero daemon endpoint")
}
pk, err := crypto.HexToECDSA(cfg.EthereumPrivateKey)
if err != nil {
return nil, err
}
ec, err := ethclient.Dial(cfg.EthereumEndpoint)
if err != nil {
return nil, err
}
pub := pk.Public().(*ecdsa.PublicKey)
pub := cfg.EthereumPrivateKey.Public().(*ecdsa.PublicKey)
addr := crypto.PubkeyToAddress(*pub)
// monero-wallet-rpc client
@@ -93,7 +83,7 @@ func NewInstance(cfg *Config) (*Instance, error) {
// open Bob's XMR wallet
if cfg.WalletFile != "" {
if err = walletClient.OpenWallet(cfg.WalletFile, cfg.WalletPassword); err != nil {
if err := walletClient.OpenWallet(cfg.WalletFile, cfg.WalletPassword); err != nil {
return nil, err
}
} else {
@@ -114,14 +104,14 @@ func NewInstance(cfg *Config) (*Instance, error) {
daemonClient: daemonClient,
walletFile: cfg.WalletFile,
walletPassword: cfg.WalletPassword,
ethClient: ec,
ethPrivKey: pk,
ethClient: cfg.EthereumClient,
ethPrivKey: cfg.EthereumPrivateKey,
callOpts: &bind.CallOpts{
From: addr,
Context: cfg.Ctx,
},
ethAddress: addr,
chainID: big.NewInt(cfg.ChainID),
chainID: cfg.ChainID,
offerManager: newOfferManager(),
swapManager: cfg.SwapManager,
}, nil

View File

@@ -12,7 +12,7 @@ import (
"github.com/noot/atomic-swap/net"
pcommon "github.com/noot/atomic-swap/protocol"
pswap "github.com/noot/atomic-swap/protocol/swap"
"github.com/noot/atomic-swap/swap-contract"
"github.com/noot/atomic-swap/swapfactory"
)
// HandleProtocolMessage is called by the network to handle an incoming message.
@@ -94,7 +94,12 @@ func (s *swapState) handleNotifyContractDeployed(msg *net.NotifyContractDeployed
return nil, errMissingAddress
}
log.Infof("got Swap contract address! address=%s", msg.Address)
if msg.ContractSwapID == nil {
return nil, errors.New("expected swapID in NotifyContractDeployed message")
}
log.Infof("got NotifyContractDeployed; address=%s contract swap ID=%d", msg.Address, msg.ContractSwapID)
s.contractSwapID = msg.ContractSwapID
if err := s.setContract(ethcommon.HexToAddress(msg.Address)); err != nil {
return nil, fmt.Errorf("failed to instantiate contract instance: %w", err)
@@ -191,7 +196,7 @@ func (s *swapState) handleRefund(txHash string) (mcrypto.Address, error) {
return "", errors.New("claim transaction has no logs")
}
sa, err := swap.GetSecretFromLog(receipt.Logs[0], "Refunded")
sa, err := swapfactory.GetSecretFromLog(receipt.Logs[0], "Refunded")
if err != nil {
return "", err
}

View File

@@ -3,6 +3,7 @@ package bob
import (
"context"
"errors"
"math/big"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
@@ -18,7 +19,7 @@ type recoveryState struct {
// NewRecoveryState returns a new *bob.recoveryState,
// which has methods to either claim ether or reclaim monero from an initiated swap.
func NewRecoveryState(b *Instance, secret *mcrypto.PrivateSpendKey,
contractAddr ethcommon.Address) (*recoveryState, error) { //nolint:revive
contractAddr ethcommon.Address, contractSwapID *big.Int) (*recoveryState, error) { //nolint:revive
txOpts, err := bind.NewKeyedTransactorWithChainID(b.ethPrivKey, b.chainID)
if err != nil {
return nil, err
@@ -39,13 +40,14 @@ func NewRecoveryState(b *Instance, secret *mcrypto.PrivateSpendKey,
ctx, cancel := context.WithCancel(b.ctx)
s := &swapState{
ctx: ctx,
cancel: cancel,
bob: b,
txOpts: txOpts,
privkeys: kp,
pubkeys: pubkp,
dleqProof: dleq.NewProofWithSecret(sc),
ctx: ctx,
cancel: cancel,
bob: b,
txOpts: txOpts,
privkeys: kp,
pubkeys: pubkp,
dleqProof: dleq.NewProofWithSecret(sc),
contractSwapID: contractSwapID,
}
if err := s.setContract(contractAddr); err != nil {

View File

@@ -21,8 +21,9 @@ func newTestRecoveryState(t *testing.T) *recoveryState {
duration, err := time.ParseDuration("1440m")
require.NoError(t, err)
addr, _ := deploySwap(t, inst, s, sr, big.NewInt(1), duration)
rs, err := NewRecoveryState(inst, s.privkeys.SpendKey(), addr)
addr, _ := newSwap(t, inst, s, [32]byte{}, sr, big.NewInt(1), duration)
rs, err := NewRecoveryState(inst, s.privkeys.SpendKey(), addr, defaultContractSwapID)
require.NoError(t, err)
return rs
@@ -33,7 +34,7 @@ func TestClaimOrRecover_Claim(t *testing.T) {
rs := newTestRecoveryState(t)
// set contract to Ready
_, err := rs.ss.contract.SetReady(rs.ss.txOpts)
_, err := rs.ss.contract.SetReady(rs.ss.txOpts, rs.ss.contractSwapID)
require.NoError(t, err)
// assert we can claim ether
@@ -58,7 +59,7 @@ func TestClaimOrRecover_Recover(t *testing.T) {
// call refund w/ Alice's spend key
sc := rs.ss.getSecret()
_, err = rs.ss.contract.Refund(rs.ss.txOpts, sc)
_, err = rs.ss.contract.Refund(rs.ss.txOpts, rs.ss.contractSwapID, sc)
require.NoError(t, err)
// assert Bob can reclaim his monero

View File

@@ -7,12 +7,11 @@ import (
"encoding/hex"
"errors"
"fmt"
"strings"
"math/big"
"sync"
"time"
eth "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
@@ -27,7 +26,7 @@ import (
"github.com/noot/atomic-swap/net"
pcommon "github.com/noot/atomic-swap/protocol"
pswap "github.com/noot/atomic-swap/protocol/swap"
"github.com/noot/atomic-swap/swap-contract"
"github.com/noot/atomic-swap/swapfactory"
)
var (
@@ -40,7 +39,7 @@ var (
var (
// this is from the autogenerated swap.go
// TODO: generate this ourselves instead of hard-coding
refundedTopic = ethcommon.HexToHash("0xfe509803c09416b28ff3d8f690c8b0c61462a892c46d5430c8fb20abe472daf0")
refundedTopic = ethcommon.HexToHash("0x4fd30f3ee0d64f7eaa62d0e005ca64c6a560652156d6c33f23ea8ca4936106e0")
)
type swapState struct {
@@ -59,10 +58,11 @@ type swapState struct {
pubkeys *mcrypto.PublicKeyPair
// swap contract and timeouts in it; set once contract is deployed
contract *swap.Swap
contractAddr ethcommon.Address
t0, t1 time.Time
txOpts *bind.TransactOpts
contract *swapfactory.SwapFactory
contractSwapID *big.Int
contractAddr ethcommon.Address
t0, t1 time.Time
txOpts *bind.TransactOpts
// Alice's keys for this session
alicePublicKeys *mcrypto.PublicKeyPair
@@ -245,7 +245,7 @@ func (s *swapState) filterForRefund() (*mcrypto.PrivateSpendKey, error) {
return nil, errNoRefundLogsFound
}
sa, err := swap.GetSecretFromLog(&logs[0], "Refunded")
sa, err := swapfactory.GetSecretFromLog(&logs[0], "Refunded")
if err != nil {
return nil, fmt.Errorf("failed to get secret from log: %w", err)
}
@@ -256,12 +256,12 @@ func (s *swapState) filterForRefund() (*mcrypto.PrivateSpendKey, error) {
func (s *swapState) tryClaim() (ethcommon.Hash, error) {
untilT0 := time.Until(s.t0)
untilT1 := time.Until(s.t1)
isReady, err := s.contract.IsReady(s.bob.callOpts)
info, err := s.contract.Swaps(s.bob.callOpts, s.contractSwapID)
if err != nil {
return ethcommon.Hash{}, err
}
if untilT0 > 0 && !isReady {
if untilT0 > 0 && !info.IsReady {
// we need to wait until t0 to claim
log.Infof("waiting until time %s to claim, time now=%s", s.t0, time.Now())
<-time.After(untilT0 + time.Second)
@@ -324,24 +324,18 @@ func (s *swapState) setAlicePublicKeys(sk *mcrypto.PublicKeyPair, secp256k1Pub *
func (s *swapState) setContract(address ethcommon.Address) error {
var err error
s.contractAddr = address
s.contract, err = swap.NewSwap(address, s.bob.ethClient)
s.contract, err = swapfactory.NewSwapFactory(address, s.bob.ethClient)
return err
}
func (s *swapState) setTimeouts() error {
st0, err := s.contract.Timeout0(s.bob.callOpts)
info, err := s.contract.Swaps(s.bob.callOpts, s.contractSwapID)
if err != nil {
return fmt.Errorf("failed to get timeout0 from contract: err=%w", err)
return fmt.Errorf("failed to get swap info from contract: err=%w", err)
}
s.t0 = time.Unix(st0.Int64(), 0)
st1, err := s.contract.Timeout1(s.bob.callOpts)
if err != nil {
return fmt.Errorf("failed to get timeout1 from contract: err=%w", err)
}
s.t1 = time.Unix(st1.Int64(), 0)
s.t0 = time.Unix(info.Timeout0.Int64(), 0)
s.t1 = time.Unix(info.Timeout1.Int64(), 0)
return nil
}
@@ -349,56 +343,57 @@ func (s *swapState) setTimeouts() error {
// if the balance doesn't match what we're expecting to receive, or the public keys in the contract
// aren't what we expect, we error and abort the swap.
func (s *swapState) checkContract() error {
balance, err := s.bob.ethClient.BalanceAt(s.ctx, s.contractAddr, nil)
if err != nil {
return err
}
expected := common.EtherToWei(s.info.ReceivedAmount()).BigInt()
if balance.Cmp(expected) < 0 {
return fmt.Errorf("contract does not have expected balance: got %s, expected %s", balance, expected)
}
constructedTopic := ethcommon.HexToHash("0x8d36aa70807342c3036697a846281194626fd4afa892356ad5979e03831ab080")
newTopic := ethcommon.HexToHash("0x982a99d883f17ecd5797205d5b3674205d7882bb28a9487d736d3799422cd055")
logs, err := s.bob.ethClient.FilterLogs(s.ctx, eth.FilterQuery{
Addresses: []ethcommon.Address{s.contractAddr},
Topics: [][]ethcommon.Hash{{constructedTopic}},
Topics: [][]ethcommon.Hash{{newTopic}},
})
if err != nil {
return fmt.Errorf("failed to filter logs: %w", err)
}
if len(logs) == 0 {
return errors.New("cannot find Constructed log")
return errors.New("cannot find New log")
}
abi, err := abi.JSON(strings.NewReader(swap.SwapABI))
if err != nil {
return err
// search for log pertaining to our swap ID
var event *swapfactory.SwapFactoryNew
for i := len(logs) - 1; i >= 0; i-- {
newEvent, err := s.contract.ParseNew(logs[i]) //nolint:govet
if err != nil {
return err
}
if newEvent.SwapID.Cmp(s.contractSwapID) == 0 {
event = newEvent
break
}
}
data := logs[0].Data
res, err := abi.Unpack("Constructed", data)
if err != nil {
return err
if event == nil {
return fmt.Errorf("failed to find New event with given swap ID %d", s.contractSwapID)
}
if len(res) < 2 {
return errors.New("constructed event was missing parameters")
}
pkClaim := res[0].([32]byte)
pkRefund := res[0].([32]byte)
// check that contract was constructed with correct secp256k1 keys
skOurs := s.secp256k1Pub.Keccak256()
if !bytes.Equal(pkClaim[:], skOurs[:]) {
return fmt.Errorf("contract claim key is not expected: got 0x%x, expected 0x%x", pkClaim, skOurs)
if !bytes.Equal(event.ClaimKey[:], skOurs[:]) {
return fmt.Errorf("contract claim key is not expected: got 0x%x, expected 0x%x", event.ClaimKey, skOurs)
}
skTheirs := s.aliceSecp256K1PublicKey.Keccak256()
if !bytes.Equal(pkRefund[:], skOurs[:]) {
return fmt.Errorf("contract claim key is not expected: got 0x%x, expected 0x%x", pkRefund, skTheirs)
if !bytes.Equal(event.RefundKey[:], skTheirs[:]) {
return fmt.Errorf("contract refund key is not expected: got 0x%x, expected 0x%x", event.RefundKey, skTheirs)
}
// check value of created swap
info, err := s.contract.Swaps(s.bob.callOpts, s.contractSwapID)
if err != nil {
return err
}
expected := common.EtherToWei(s.info.ReceivedAmount()).BigInt()
if info.Value.Cmp(expected) < 0 {
return fmt.Errorf("contract does not have expected balance: got %s, expected %s", info.Value, expected)
}
return nil
@@ -465,7 +460,7 @@ func (s *swapState) claimFunds() (ethcommon.Hash, error) {
// call swap.Swap.Claim() w/ b.privkeys.sk, revealing Bob's secret spend key
sc := s.getSecret()
tx, err := s.contract.Claim(s.txOpts, sc)
tx, err := s.contract.Claim(s.txOpts, s.contractSwapID, sc)
if err != nil {
return ethcommon.Hash{}, err
}

View File

@@ -12,9 +12,10 @@ import (
"github.com/noot/atomic-swap/net"
pcommon "github.com/noot/atomic-swap/protocol"
pswap "github.com/noot/atomic-swap/protocol/swap"
"github.com/noot/atomic-swap/swap-contract"
"github.com/noot/atomic-swap/swapfactory"
ethcommon "github.com/ethereum/go-ethereum/common"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
logging "github.com/ipfs/go-log"
"github.com/stretchr/testify/require"
@@ -35,9 +36,18 @@ func (n *mockNet) SendSwapMessage(msg net.Message) error {
return nil
}
var defaultTimeoutDuration = big.NewInt(60 * 60 * 24) // 1 day = 60s * 60min * 24hr
var (
defaultTimeoutDuration, _ = time.ParseDuration("86400s") // 1 day = 60s * 60min * 24hr
defaultContractSwapID = big.NewInt(0)
)
func newTestInstance(t *testing.T) (*Instance, *swapState) {
pk, err := ethcrypto.HexToECDSA(common.DefaultPrivKeyBob)
require.NoError(t, err)
ec, err := ethclient.Dial(common.DefaultEthEndpoint)
require.NoError(t, err)
cfg := &Config{
Ctx: context.Background(),
Basepath: "/tmp/bob",
@@ -45,10 +55,10 @@ func newTestInstance(t *testing.T) (*Instance, *swapState) {
MoneroDaemonEndpoint: common.DefaultMoneroDaemonEndpoint,
WalletFile: testWallet,
WalletPassword: "",
EthereumEndpoint: common.DefaultEthEndpoint,
EthereumPrivateKey: common.DefaultPrivKeyBob,
EthereumClient: ec,
EthereumPrivateKey: pk,
Environment: common.Development,
ChainID: common.MainnetConfig.EthereumChainID,
ChainID: big.NewInt(common.MainnetConfig.EthereumChainID),
SwapManager: pswap.NewManager(),
}
@@ -79,6 +89,33 @@ func newTestAliceSendKeySMessage(t *testing.T) (*net.SendKeysMessage, *pcommon.K
return msg, keysAndProof
}
func newSwap(t *testing.T, bob *Instance, swapState *swapState, claimKey, refundKey [32]byte, amount *big.Int,
timeout time.Duration) (ethcommon.Address, *swapfactory.SwapFactory) {
tm := big.NewInt(int64(timeout.Seconds()))
if claimKey == [32]byte{} {
claimKey = swapState.secp256k1Pub.Keccak256()
}
addr, _, contract, err := swapfactory.DeploySwapFactory(swapState.txOpts, bob.ethClient)
require.NoError(t, err)
swapState.txOpts.Value = amount
defer func() {
swapState.txOpts.Value = nil
}()
tx, err := contract.NewSwap(swapState.txOpts, claimKey, refundKey, bob.ethAddress, tm)
require.NoError(t, err)
receipt, err := bob.ethClient.TransactionReceipt(context.Background(), tx.Hash())
require.NoError(t, err)
require.Equal(t, 1, len(receipt.Logs))
swapState.contractSwapID, err = swapfactory.GetIDFromLog(receipt.Logs[0])
require.NoError(t, err)
return addr, contract
}
func TestSwapState_GenerateAndSetKeys(t *testing.T) {
_, swapState := newTestInstance(t)
@@ -94,15 +131,11 @@ func TestSwapState_ClaimFunds(t *testing.T) {
err := swapState.generateAndSetKeys()
require.NoError(t, err)
conn, err := ethclient.Dial(common.DefaultEthEndpoint)
require.NoError(t, err)
claimKey := swapState.secp256k1Pub.Keccak256()
swapState.contractAddr, _, swapState.contract, err = swap.DeploySwap(swapState.txOpts, conn,
claimKey, [32]byte{}, bob.ethAddress, defaultTimeoutDuration)
require.NoError(t, err)
swapState.contractAddr, swapState.contract = newSwap(t, bob, swapState, claimKey,
[32]byte{}, big.NewInt(33), defaultTimeoutDuration)
_, err = swapState.contract.SetReady(swapState.txOpts)
_, err = swapState.contract.SetReady(swapState.txOpts, defaultContractSwapID)
require.NoError(t, err)
txHash, err := swapState.claimFunds()
@@ -127,25 +160,6 @@ func TestSwapState_handleSendKeysMessage(t *testing.T) {
require.Equal(t, alicePubKeys.ViewKey().Hex(), s.alicePublicKeys.ViewKey().Hex())
}
func deploySwap(t *testing.T, bob *Instance, swapState *swapState, refundKey [32]byte, amount *big.Int,
timeout time.Duration) (ethcommon.Address, *swap.Swap) {
conn, err := ethclient.Dial(common.DefaultEthEndpoint)
require.NoError(t, err)
tm := big.NewInt(int64(timeout.Seconds()))
claimKey := swapState.secp256k1Pub.Keccak256()
swapState.txOpts.Value = amount
defer func() {
swapState.txOpts.Value = nil
}()
addr, _, contract, err := swap.DeploySwap(swapState.txOpts, conn, claimKey, refundKey, bob.ethAddress, tm)
require.NoError(t, err)
return addr, contract
}
func TestSwapState_HandleProtocolMessage_NotifyContractDeployed_ok(t *testing.T) {
bob, s := newTestInstance(t)
defer s.cancel()
@@ -165,10 +179,12 @@ func TestSwapState_HandleProtocolMessage_NotifyContractDeployed_ok(t *testing.T)
duration, err := time.ParseDuration("2s")
require.NoError(t, err)
addr, _ := deploySwap(t, bob, s, [32]byte{}, desiredAmout.BigInt(), duration)
addr, _ := newSwap(t, bob, s, s.secp256k1Pub.Keccak256(), s.aliceSecp256K1PublicKey.Keccak256(),
desiredAmout.BigInt(), duration)
msg = &net.NotifyContractDeployed{
Address: addr.String(),
Address: addr.String(),
ContractSwapID: defaultContractSwapID,
}
resp, done, err = s.HandleProtocolMessage(msg)
@@ -202,10 +218,12 @@ func TestSwapState_HandleProtocolMessage_NotifyContractDeployed_timeout(t *testi
duration, err := time.ParseDuration("15s")
require.NoError(t, err)
addr, _ := deploySwap(t, bob, s, [32]byte{}, desiredAmout.BigInt(), duration)
addr, _ := newSwap(t, bob, s, s.secp256k1Pub.Keccak256(), s.aliceSecp256K1PublicKey.Keccak256(),
desiredAmout.BigInt(), duration)
msg = &net.NotifyContractDeployed{
Address: addr.String(),
Address: addr.String(),
ContractSwapID: defaultContractSwapID,
}
resp, done, err = s.HandleProtocolMessage(msg)
@@ -232,9 +250,9 @@ func TestSwapState_HandleProtocolMessage_NotifyReady(t *testing.T) {
duration, err := time.ParseDuration("10m")
require.NoError(t, err)
_, s.contract = deploySwap(t, bob, s, [32]byte{}, desiredAmout.BigInt(), duration)
_, s.contract = newSwap(t, bob, s, [32]byte{}, [32]byte{}, desiredAmout.BigInt(), duration)
_, err = s.contract.SetReady(s.txOpts)
_, err = s.contract.SetReady(s.txOpts, defaultContractSwapID)
require.NoError(t, err)
msg := &net.NotifyReady{}
@@ -260,7 +278,7 @@ func TestSwapState_handleRefund(t *testing.T) {
require.NoError(t, err)
refundKey := aliceKeysAndProof.Secp256k1PublicKey.Keccak256()
_, s.contract = deploySwap(t, bob, s, refundKey, desiredAmout.BigInt(), duration)
_, s.contract = newSwap(t, bob, s, [32]byte{}, refundKey, desiredAmout.BigInt(), duration)
// lock XMR
addrAB, err := s.lockFunds(common.MoneroToPiconero(s.info.ProvidedAmount()))
@@ -271,7 +289,7 @@ func TestSwapState_handleRefund(t *testing.T) {
var sc [32]byte
copy(sc[:], common.Reverse(secret))
tx, err := s.contract.Refund(s.txOpts, sc)
tx, err := s.contract.Refund(s.txOpts, defaultContractSwapID, sc)
require.NoError(t, err)
addr, err := s.handleRefund(tx.Hash().String())
@@ -293,7 +311,7 @@ func TestSwapState_HandleProtocolMessage_NotifyRefund(t *testing.T) {
require.NoError(t, err)
refundKey := aliceKeysAndProof.Secp256k1PublicKey.Keccak256()
_, s.contract = deploySwap(t, bob, s, refundKey, desiredAmout.BigInt(), duration)
_, s.contract = newSwap(t, bob, s, [32]byte{}, refundKey, desiredAmout.BigInt(), duration)
// lock XMR
_, err = s.lockFunds(common.MoneroToPiconero(s.info.ProvidedAmount()))
@@ -304,7 +322,7 @@ func TestSwapState_HandleProtocolMessage_NotifyRefund(t *testing.T) {
var sc [32]byte
copy(sc[:], common.Reverse(secret[:]))
tx, err := s.contract.Refund(s.txOpts, sc)
tx, err := s.contract.Refund(s.txOpts, defaultContractSwapID, sc)
require.NoError(t, err)
msg := &net.NotifyRefund{
@@ -332,7 +350,7 @@ func TestSwapState_ProtocolExited_Reclaim(t *testing.T) {
require.NoError(t, err)
refundKey := aliceKeysAndProof.Secp256k1PublicKey.Keccak256()
s.contractAddr, s.contract = deploySwap(t, bob, s, refundKey, desiredAmout.BigInt(), duration)
s.contractAddr, s.contract = newSwap(t, bob, s, [32]byte{}, refundKey, desiredAmout.BigInt(), duration)
// lock XMR
_, err = s.lockFunds(common.MoneroToPiconero(s.info.ProvidedAmount()))
@@ -343,7 +361,7 @@ func TestSwapState_ProtocolExited_Reclaim(t *testing.T) {
var sc [32]byte
copy(sc[:], common.Reverse(secret[:]))
tx, err := s.contract.Refund(s.txOpts, sc)
tx, err := s.contract.Refund(s.txOpts, defaultContractSwapID, sc)
require.NoError(t, err)
receipt, err := bob.ethClient.TransactionReceipt(s.ctx, tx.Hash())

View File

@@ -3,6 +3,7 @@ package recovery
import (
"encoding/hex"
"fmt"
"math/big"
"github.com/noot/atomic-swap/common"
mcrypto "github.com/noot/atomic-swap/crypto/monero"
@@ -67,7 +68,7 @@ func (r *recoverer) WalletFromSecrets(aliceSecret, bobSecret string) (mcrypto.Ad
// RecoverFromBobSecretAndContract recovers funds by either claiming ether or reclaiming locked monero.
func (r *recoverer) RecoverFromBobSecretAndContract(b *bob.Instance,
bobSecret, contractAddr string) (*bob.RecoveryResult, error) {
bobSecret, contractAddr string, swapID *big.Int) (*bob.RecoveryResult, error) {
bs, err := hex.DecodeString(bobSecret)
if err != nil {
return nil, fmt.Errorf("failed to decode Bob's secret: %w", err)
@@ -79,7 +80,7 @@ func (r *recoverer) RecoverFromBobSecretAndContract(b *bob.Instance,
}
addr := ethcommon.HexToAddress(contractAddr)
rs, err := bob.NewRecoveryState(b, bk, addr)
rs, err := bob.NewRecoveryState(b, bk, addr, swapID)
if err != nil {
return nil, err
}
@@ -89,7 +90,7 @@ func (r *recoverer) RecoverFromBobSecretAndContract(b *bob.Instance,
// RecoverFromAliceSecretAndContract recovers funds by either claiming locked monero or refunding ether.
func (r *recoverer) RecoverFromAliceSecretAndContract(a *alice.Instance,
aliceSecret, contractAddr string) (*alice.RecoveryResult, error) {
aliceSecret, contractAddr string, swapID *big.Int) (*alice.RecoveryResult, error) {
as, err := hex.DecodeString(aliceSecret)
if err != nil {
return nil, fmt.Errorf("failed to decode Alice's secret: %w", err)
@@ -101,7 +102,7 @@ func (r *recoverer) RecoverFromAliceSecretAndContract(a *alice.Instance,
}
addr := ethcommon.HexToAddress(contractAddr)
rs, err := alice.NewRecoveryState(a, ak, addr)
rs, err := alice.NewRecoveryState(a, ak, addr, swapID)
if err != nil {
return nil, err
}

View File

@@ -1,6 +1,6 @@
#!/bin/bash
$SOLC_BIN --abi ethereum/contracts/Swap.sol -o ethereum/abi/ --overwrite
$SOLC_BIN --bin ethereum/contracts/Swap.sol -o ethereum/bin/ --overwrite
abigen --abi ethereum/abi/Swap.abi --pkg swap --type Swap --out swap.go --bin ethereum/bin/Swap.bin
mv swap.go ./swap-contract
$SOLC_BIN --abi ethereum/contracts/SwapFactory.sol -o ethereum/abi/ --overwrite
$SOLC_BIN --bin ethereum/contracts/SwapFactory.sol -o ethereum/bin/ --overwrite
abigen --abi ethereum/abi/SwapFactory.abi --pkg swapfactory --type SwapFactory --out swap_factory.go --bin ethereum/bin/SwapFactory.bin
mv swap_factory.go ./swapfactory

File diff suppressed because one or more lines are too long

View File

@@ -1,190 +0,0 @@
package swap
import (
"crypto/ecdsa"
"encoding/hex"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/require"
"github.com/noot/atomic-swap/common"
"github.com/noot/atomic-swap/crypto/secp256k1"
"github.com/noot/atomic-swap/dleq"
)
var defaultTimeoutDuration = big.NewInt(60) // 60 seconds
func setupAliceAuth(t *testing.T) (*bind.TransactOpts, *ethclient.Client, *ecdsa.PrivateKey) {
conn, err := ethclient.Dial(common.DefaultEthEndpoint)
require.NoError(t, err)
pkA, err := crypto.HexToECDSA(common.DefaultPrivKeyAlice)
require.NoError(t, err)
auth, err := bind.NewKeyedTransactorWithChainID(pkA, big.NewInt(common.GanacheChainID))
require.NoError(t, err)
return auth, conn, pkA
}
func TestDeploySwap(t *testing.T) {
auth, conn, _ := setupAliceAuth(t)
address, tx, swapContract, err := DeploySwap(auth, conn, [32]byte{}, [32]byte{},
ethcommon.Address{}, defaultTimeoutDuration)
require.NoError(t, err)
require.NotEqual(t, ethcommon.Address{}, address)
require.NotNil(t, tx)
require.NotNil(t, swapContract)
}
func TestSwap_Claim_vec(t *testing.T) {
secret, err := hex.DecodeString("D30519BCAE8D180DBFCC94FE0B8383DC310185B0BE97B4365083EBCECCD75759")
require.NoError(t, err)
pubX, err := hex.DecodeString("3AF1E1EFA4D1E1AD5CB9E3967E98E901DAFCD37C44CF0BFB6C216997F5EE51DF")
require.NoError(t, err)
pubY, err := hex.DecodeString("E4ACAC3E6F139E0C7DB2BD736824F51392BDA176965A1C59EB9C3C5FF9E85D7A")
require.NoError(t, err)
var s, x, y [32]byte
copy(s[:], secret)
copy(x[:], pubX)
copy(y[:], pubY)
pk := secp256k1.NewPublicKey(x, y)
cmt := pk.Keccak256()
// deploy swap contract with claim key hash
auth, conn, pkA := setupAliceAuth(t)
pub := pkA.Public().(*ecdsa.PublicKey)
addr := crypto.PubkeyToAddress(*pub)
t.Logf("commitment: 0x%x", cmt)
_, deployTx, swap, err := DeploySwap(auth, conn, cmt, [32]byte{}, addr,
defaultTimeoutDuration)
require.NoError(t, err)
t.Logf("gas cost to deploy Swap.sol: %d", deployTx.Gas())
// set contract to Ready
_, err = swap.SetReady(auth)
require.NoError(t, err)
// now let's try to claim
tx, err := swap.Claim(auth, s)
require.NoError(t, err)
t.Log(tx.Hash())
}
func TestSwap_Claim_random(t *testing.T) {
// generate claim secret and public key
dleq := &dleq.FarcasterDLEq{}
proof, err := dleq.Prove()
require.NoError(t, err)
res, err := dleq.Verify(proof)
require.NoError(t, err)
// hash public key
cmt := res.Secp256k1PublicKey().Keccak256()
// deploy swap contract with claim key hash
auth, conn, pkA := setupAliceAuth(t)
pub := pkA.Public().(*ecdsa.PublicKey)
addr := crypto.PubkeyToAddress(*pub)
_, deployTx, swap, err := DeploySwap(auth, conn, cmt, [32]byte{}, addr,
defaultTimeoutDuration)
require.NoError(t, err)
t.Logf("gas cost to deploy Swap.sol: %d", deployTx.Gas())
// set contract to Ready
tx, err := swap.SetReady(auth)
require.NoError(t, err)
t.Logf("gas cost to call SetReady: %d", tx.Gas())
// now let's try to claim
var s [32]byte
secret := proof.Secret()
copy(s[:], common.Reverse(secret[:]))
tx, err = swap.Claim(auth, s)
require.NoError(t, err)
t.Logf("gas cost to call Claim: %d", tx.Gas())
}
func TestSwap_Refund_beforeT0(t *testing.T) {
// generate refund secret and public key
dleq := &dleq.FarcasterDLEq{}
proof, err := dleq.Prove()
require.NoError(t, err)
res, err := dleq.Verify(proof)
require.NoError(t, err)
// hash public key
cmt := res.Secp256k1PublicKey().Keccak256()
// deploy swap contract with claim key hash
auth, conn, pkA := setupAliceAuth(t)
pub := pkA.Public().(*ecdsa.PublicKey)
addr := crypto.PubkeyToAddress(*pub)
_, deployTx, swap, err := DeploySwap(auth, conn, [32]byte{}, cmt, addr,
defaultTimeoutDuration)
require.NoError(t, err)
t.Logf("gas cost to deploy Swap.sol: %d", deployTx.Gas())
// now let's try to claim
var s [32]byte
secret := proof.Secret()
copy(s[:], common.Reverse(secret[:]))
tx, err := swap.Refund(auth, s)
require.NoError(t, err)
t.Logf("gas cost to call Refund: %d", tx.Gas())
}
func TestSwap_Refund_afterT1(t *testing.T) {
// generate refund secret and public key
dleq := &dleq.FarcasterDLEq{}
proof, err := dleq.Prove()
require.NoError(t, err)
res, err := dleq.Verify(proof)
require.NoError(t, err)
// hash public key
cmt := res.Secp256k1PublicKey().Keccak256()
// deploy swap contract with claim key hash
auth, conn, pkA := setupAliceAuth(t)
pub := pkA.Public().(*ecdsa.PublicKey)
addr := crypto.PubkeyToAddress(*pub)
_, deployTx, swap, err := DeploySwap(auth, conn, [32]byte{}, cmt, addr,
defaultTimeoutDuration)
require.NoError(t, err)
t.Logf("gas cost to deploy Swap.sol: %d", deployTx.Gas())
// fast forward past t1
rpcClient, err := rpc.Dial(common.DefaultEthEndpoint)
require.NoError(t, err)
var result string
err = rpcClient.Call(&result, "evm_snapshot")
require.NoError(t, err)
err = rpcClient.Call(nil, "evm_increaseTime", defaultTimeoutDuration.Int64()*2+60)
require.NoError(t, err)
defer func() {
var ok bool
err = rpcClient.Call(&ok, "evm_revert", result)
require.NoError(t, err)
}()
// now let's try to claim
var s [32]byte
secret := proof.Secret()
copy(s[:], common.Reverse(secret[:]))
tx, err := swap.Refund(auth, s)
require.NoError(t, err)
t.Logf("gas cost to call Refund: %d", tx.Gas())
}

893
swapfactory/swap_factory.go Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,342 @@
package swapfactory
import (
"context"
"crypto/ecdsa"
"encoding/hex"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/require"
"github.com/noot/atomic-swap/common"
"github.com/noot/atomic-swap/crypto/secp256k1"
"github.com/noot/atomic-swap/dleq"
)
var defaultTimeoutDuration = big.NewInt(60) // 60 seconds
func setupAliceAuth(t *testing.T) (*bind.TransactOpts, *ethclient.Client, *ecdsa.PrivateKey) {
conn, err := ethclient.Dial(common.DefaultEthEndpoint)
require.NoError(t, err)
pkA, err := crypto.HexToECDSA(common.DefaultPrivKeyAlice)
require.NoError(t, err)
auth, err := bind.NewKeyedTransactorWithChainID(pkA, big.NewInt(common.GanacheChainID))
require.NoError(t, err)
return auth, conn, pkA
}
func TestSwapFactory_NewSwap(t *testing.T) {
auth, conn, _ := setupAliceAuth(t)
address, tx, contract, err := DeploySwapFactory(auth, conn)
require.NoError(t, err)
require.NotEqual(t, ethcommon.Address{}, address)
require.NotNil(t, tx)
require.NotNil(t, contract)
t.Logf("gas cost to deploy SwapFactory.sol: %d", tx.Gas())
tx, err = contract.NewSwap(auth, [32]byte{}, [32]byte{},
ethcommon.Address{}, defaultTimeoutDuration)
require.NoError(t, err)
t.Logf("gas cost to call new_swap: %d", tx.Gas())
}
func TestSwapFactory_Claim_vec(t *testing.T) {
secret, err := hex.DecodeString("D30519BCAE8D180DBFCC94FE0B8383DC310185B0BE97B4365083EBCECCD75759")
require.NoError(t, err)
pubX, err := hex.DecodeString("3AF1E1EFA4D1E1AD5CB9E3967E98E901DAFCD37C44CF0BFB6C216997F5EE51DF")
require.NoError(t, err)
pubY, err := hex.DecodeString("E4ACAC3E6F139E0C7DB2BD736824F51392BDA176965A1C59EB9C3C5FF9E85D7A")
require.NoError(t, err)
var s, x, y [32]byte
copy(s[:], secret)
copy(x[:], pubX)
copy(y[:], pubY)
pk := secp256k1.NewPublicKey(x, y)
cmt := pk.Keccak256()
// deploy swap contract with claim key hash
auth, conn, pkA := setupAliceAuth(t)
pub := pkA.Public().(*ecdsa.PublicKey)
addr := crypto.PubkeyToAddress(*pub)
t.Logf("commitment: 0x%x", cmt)
_, tx, contract, err := DeploySwapFactory(auth, conn)
require.NoError(t, err)
t.Logf("gas cost to deploy SwapFactory.sol: %d", tx.Gas())
tx, err = contract.NewSwap(auth, cmt, [32]byte{}, addr,
defaultTimeoutDuration)
require.NoError(t, err)
t.Logf("gas cost to call new_swap: %d", tx.Gas())
receipt, err := conn.TransactionReceipt(context.Background(), tx.Hash())
require.NoError(t, err)
require.Equal(t, 1, len(receipt.Logs))
id, err := GetIDFromLog(receipt.Logs[0])
require.NoError(t, err)
// set contract to Ready
tx, err = contract.SetReady(auth, id)
require.NoError(t, err)
t.Logf("gas cost to call set_ready: %d", tx.Gas())
// now let's try to claim
tx, err = contract.Claim(auth, id, s)
require.NoError(t, err)
t.Logf("gas cost to call claim: %d", tx.Gas())
callOpts := &bind.CallOpts{
From: crypto.PubkeyToAddress(*pub),
Context: context.Background(),
}
info, err := contract.Swaps(callOpts, id)
require.NoError(t, err)
require.True(t, info.Completed)
}
func TestSwap_Claim_random(t *testing.T) {
// generate claim secret and public key
dleq := &dleq.FarcasterDLEq{}
proof, err := dleq.Prove()
require.NoError(t, err)
res, err := dleq.Verify(proof)
require.NoError(t, err)
// hash public key
cmt := res.Secp256k1PublicKey().Keccak256()
// deploy swap contract with claim key hash
auth, conn, pkA := setupAliceAuth(t)
pub := pkA.Public().(*ecdsa.PublicKey)
addr := crypto.PubkeyToAddress(*pub)
_, tx, contract, err := DeploySwapFactory(auth, conn)
require.NoError(t, err)
t.Logf("gas cost to deploy SwapFactory.sol: %d", tx.Gas())
tx, err = contract.NewSwap(auth, cmt, [32]byte{}, addr,
defaultTimeoutDuration)
require.NoError(t, err)
t.Logf("gas cost to call new_swap: %d", tx.Gas())
receipt, err := conn.TransactionReceipt(context.Background(), tx.Hash())
require.NoError(t, err)
require.Equal(t, 1, len(receipt.Logs))
id, err := GetIDFromLog(receipt.Logs[0])
require.NoError(t, err)
// set contract to Ready
tx, err = contract.SetReady(auth, id)
require.NoError(t, err)
t.Logf("gas cost to call SetReady: %d", tx.Gas())
// now let's try to claim
var s [32]byte
secret := proof.Secret()
copy(s[:], common.Reverse(secret[:]))
tx, err = contract.Claim(auth, id, s)
require.NoError(t, err)
t.Logf("gas cost to call Claim: %d", tx.Gas())
callOpts := &bind.CallOpts{
From: crypto.PubkeyToAddress(*pub),
Context: context.Background(),
}
info, err := contract.Swaps(callOpts, id)
require.NoError(t, err)
require.True(t, info.Completed)
}
func TestSwap_Refund_beforeT0(t *testing.T) {
// generate refund secret and public key
dleq := &dleq.FarcasterDLEq{}
proof, err := dleq.Prove()
require.NoError(t, err)
res, err := dleq.Verify(proof)
require.NoError(t, err)
// hash public key
cmt := res.Secp256k1PublicKey().Keccak256()
// deploy swap contract with refund key hash
auth, conn, pkA := setupAliceAuth(t)
pub := pkA.Public().(*ecdsa.PublicKey)
addr := crypto.PubkeyToAddress(*pub)
_, tx, contract, err := DeploySwapFactory(auth, conn)
require.NoError(t, err)
t.Logf("gas cost to deploy SwapFactory.sol: %d", tx.Gas())
tx, err = contract.NewSwap(auth, [32]byte{}, cmt, addr,
defaultTimeoutDuration)
require.NoError(t, err)
t.Logf("gas cost to call new_swap: %d", tx.Gas())
receipt, err := conn.TransactionReceipt(context.Background(), tx.Hash())
require.NoError(t, err)
require.Equal(t, 1, len(receipt.Logs))
id, err := GetIDFromLog(receipt.Logs[0])
require.NoError(t, err)
// now let's try to refund
var s [32]byte
secret := proof.Secret()
copy(s[:], common.Reverse(secret[:]))
tx, err = contract.Refund(auth, id, s)
require.NoError(t, err)
t.Logf("gas cost to call Refund: %d", tx.Gas())
callOpts := &bind.CallOpts{
From: crypto.PubkeyToAddress(*pub),
Context: context.Background(),
}
info, err := contract.Swaps(callOpts, id)
require.NoError(t, err)
require.True(t, info.Completed)
}
func TestSwap_Refund_afterT1(t *testing.T) {
// generate refund secret and public key
dleq := &dleq.FarcasterDLEq{}
proof, err := dleq.Prove()
require.NoError(t, err)
res, err := dleq.Verify(proof)
require.NoError(t, err)
// hash public key
cmt := res.Secp256k1PublicKey().Keccak256()
// deploy swap contract with refund key hash
auth, conn, pkA := setupAliceAuth(t)
pub := pkA.Public().(*ecdsa.PublicKey)
addr := crypto.PubkeyToAddress(*pub)
_, tx, contract, err := DeploySwapFactory(auth, conn)
require.NoError(t, err)
t.Logf("gas cost to deploy SwapFactory.sol: %d", tx.Gas())
tx, err = contract.NewSwap(auth, [32]byte{}, cmt, addr,
defaultTimeoutDuration)
require.NoError(t, err)
t.Logf("gas cost to call new_swap: %d", tx.Gas())
receipt, err := conn.TransactionReceipt(context.Background(), tx.Hash())
require.NoError(t, err)
require.Equal(t, 1, len(receipt.Logs))
id, err := GetIDFromLog(receipt.Logs[0])
require.NoError(t, err)
// fast forward past t1
rpcClient, err := rpc.Dial(common.DefaultEthEndpoint)
require.NoError(t, err)
var result string
err = rpcClient.Call(&result, "evm_snapshot")
require.NoError(t, err)
err = rpcClient.Call(nil, "evm_increaseTime", defaultTimeoutDuration.Int64()*2+60)
require.NoError(t, err)
defer func() {
var ok bool
err = rpcClient.Call(&ok, "evm_revert", result)
require.NoError(t, err)
}()
// now let's try to refund
var s [32]byte
secret := proof.Secret()
copy(s[:], common.Reverse(secret[:]))
tx, err = contract.Refund(auth, id, s)
require.NoError(t, err)
t.Logf("gas cost to call Refund: %d", tx.Gas())
callOpts := &bind.CallOpts{
From: crypto.PubkeyToAddress(*pub),
Context: context.Background(),
}
info, err := contract.Swaps(callOpts, id)
require.NoError(t, err)
require.True(t, info.Completed)
}
func TestSwap_MultipleSwaps(t *testing.T) {
// test case where contract has multiple swaps happening at once
auth, conn, pkA := setupAliceAuth(t)
pub := pkA.Public().(*ecdsa.PublicKey)
addr := crypto.PubkeyToAddress(*pub)
_, tx, contract, err := DeploySwapFactory(auth, conn)
require.NoError(t, err)
t.Logf("gas cost to deploy SwapFactory.sol: %d", tx.Gas())
numSwaps := 16
type swapCase struct {
id *big.Int
secret [32]byte
}
// setup all swap instances in contract
swapCases := []*swapCase{}
for i := 0; i < numSwaps; i++ {
sc := &swapCase{}
// generate claim secret and public key
dleq := &dleq.FarcasterDLEq{}
proof, err := dleq.Prove() //nolint:govet
require.NoError(t, err)
res, err := dleq.Verify(proof)
require.NoError(t, err)
// hash public key
cmt := res.Secp256k1PublicKey().Keccak256()
secret := proof.Secret()
copy(sc.secret[:], common.Reverse(secret[:]))
tx, err = contract.NewSwap(auth, cmt, [32]byte{}, addr,
defaultTimeoutDuration)
require.NoError(t, err)
t.Logf("gas cost to call new_swap: %d", tx.Gas())
receipt, err := conn.TransactionReceipt(context.Background(), tx.Hash())
require.NoError(t, err)
require.Equal(t, 1, len(receipt.Logs))
sc.id, err = GetIDFromLog(receipt.Logs[0])
require.NoError(t, err)
swapCases = append(swapCases, sc)
}
for _, sc := range swapCases {
// set contract to Ready
tx, err = contract.SetReady(auth, sc.id)
require.NoError(t, err)
t.Logf("gas cost to call SetReady: %d", tx.Gas())
// now let's try to claim
tx, err = contract.Claim(auth, sc.id, sc.secret)
require.NoError(t, err)
t.Logf("gas cost to call Claim: %d", tx.Gas())
callOpts := &bind.CallOpts{
From: crypto.PubkeyToAddress(*pub),
Context: context.Background(),
}
info, err := contract.Swaps(callOpts, sc.id)
require.NoError(t, err)
require.True(t, info.Completed)
}
}

View File

@@ -1,7 +1,8 @@
package swap
package swapfactory
import (
"errors"
"math/big"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
@@ -17,7 +18,7 @@ func GetSecretFromLog(log *ethtypes.Log, event string) (*mcrypto.PrivateSpendKey
return nil, errors.New("invalid event name, must be one of Claimed or Refunded")
}
abi, err := abi.JSON(strings.NewReader(SwapABI))
abi, err := abi.JSON(strings.NewReader(SwapFactoryABI))
if err != nil {
return nil, err
}
@@ -28,11 +29,11 @@ func GetSecretFromLog(log *ethtypes.Log, event string) (*mcrypto.PrivateSpendKey
return nil, err
}
if len(res) == 0 {
return nil, errors.New("log had no parameters")
if len(res) < 2 {
return nil, errors.New("log had not enough parameters")
}
s := res[0].([32]byte)
s := res[1].([32]byte)
sk, err := mcrypto.NewPrivateSpendKey(common.Reverse(s[:]))
if err != nil {
@@ -41,3 +42,26 @@ func GetSecretFromLog(log *ethtypes.Log, event string) (*mcrypto.PrivateSpendKey
return sk, nil
}
// GetIDFromLog returns the swap ID from a New log.
func GetIDFromLog(log *ethtypes.Log) (*big.Int, error) {
abi, err := abi.JSON(strings.NewReader(SwapFactoryABI))
if err != nil {
return nil, err
}
const event = "New"
data := log.Data
res, err := abi.Unpack(event, data)
if err != nil {
return nil, err
}
if len(res) == 0 {
return nil, errors.New("log had not enough parameters")
}
id := res[0].(*big.Int)
return id, nil
}