mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-08 21:58:07 -05:00
implement SwapFactory.sol and integrate into codebase (#85)
This commit is contained in:
61
cmd/daemon/contract.go
Normal file
61
cmd/daemon/contract.go
Normal 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)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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")),
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
133
ethereum/contracts/SwapFactory.sol
Normal file
133
ethereum/contracts/SwapFactory.sol
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
13
ethereum/scripts/deploy.js
Normal file
13
ethereum/scripts/deploy.js
Normal 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);
|
||||
});
|
||||
@@ -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 ...
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
893
swapfactory/swap_factory.go
Normal file
File diff suppressed because one or more lines are too long
342
swapfactory/swap_factory_test.go
Normal file
342
swapfactory/swap_factory_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user