mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-07 21:34:05 -05:00
USDT compatibility (#484)
This commit is contained in:
5
Makefile
5
Makefile
@@ -14,7 +14,7 @@ lint-shell:
|
||||
|
||||
.PHONY: lint-solidity
|
||||
lint-solidity:
|
||||
"$$(npm config get prefix)/bin/solhint" $$(find ethereum -name '*.sol')
|
||||
"$$(npm config get prefix)/bin/solhint" $$(find ethereum -name '*.sol' -not -path '**/@openzeppelin/**')
|
||||
|
||||
.PHONY: lint
|
||||
lint: lint-go lint-shell lint-solidity
|
||||
@@ -30,7 +30,8 @@ format-shell:
|
||||
|
||||
.PHONY: format-solidity
|
||||
format-solidity:
|
||||
"$$(npm config get prefix)/bin/prettier" --print-width 100 --write $$(find ethereum -name '*.sol')
|
||||
"$$(npm config get prefix)/bin/prettier" --print-width 100 --write \
|
||||
$$(find ethereum -name '*.sol' -not -path '**/@openzeppelin/**')
|
||||
|
||||
.PHONY: format
|
||||
format: format-go format-shell format-solidity
|
||||
|
||||
@@ -1139,8 +1139,8 @@ func runGetContractSwapInfo(ctx *cli.Context) error {
|
||||
fmt.Printf("Swap struct as stored in the contract:\n")
|
||||
fmt.Printf("\tOwner: %s\n", resp.Swap.Owner)
|
||||
fmt.Printf("\tClaimer: %s\n", resp.Swap.Claimer)
|
||||
fmt.Printf("\tPubKeyClaim: %x\n", resp.Swap.PubKeyClaim)
|
||||
fmt.Printf("\tPubKeyRefund: %x\n", resp.Swap.PubKeyRefund)
|
||||
fmt.Printf("\tClaimCommitment: %x\n", resp.Swap.ClaimCommitment)
|
||||
fmt.Printf("\tRefundCommitment: %x\n", resp.Swap.RefundCommitment)
|
||||
fmt.Printf("\tTimeout1: %s\n", resp.Swap.Timeout1)
|
||||
fmt.Printf("\tTimeout2: %s\n", resp.Swap.Timeout2)
|
||||
fmt.Printf("\tAsset: %s\n", resp.Swap.Asset)
|
||||
|
||||
@@ -66,7 +66,7 @@ func MainnetConfig() *Config {
|
||||
Port: DefaultMoneroDaemonMainnetPort,
|
||||
},
|
||||
},
|
||||
SwapCreatorAddr: ethcommon.HexToAddress("0xa55aa5557ec22e85804729bc6935029bb84cf16a"),
|
||||
SwapCreatorAddr: ethcommon.HexToAddress("0x377ed3a60007048DF00135637521170628De89E5"),
|
||||
Bootnodes: []string{
|
||||
"/ip4/67.205.131.11/tcp/9909/p2p/12D3KooWGpCLC4y42rf6aR3cguVFJAruzFXT6mUEyp7C32jTsyJd",
|
||||
"/ip4/143.198.123.27/tcp/9909/p2p/12D3KooWDCE2ukB1Sw88hmLFk5BZRRViyYLeuAKPuu59nYyFWAec",
|
||||
@@ -99,7 +99,7 @@ func StagenetConfig() *Config {
|
||||
Port: 38081,
|
||||
},
|
||||
},
|
||||
SwapCreatorAddr: ethcommon.HexToAddress("0xbf2B7a6dCE5598Cf002B3507a8D62cf2C35cE5c6"),
|
||||
SwapCreatorAddr: ethcommon.HexToAddress("0x377ed3a60007048DF00135637521170628De89E5"),
|
||||
Bootnodes: []string{
|
||||
"/ip4/134.122.115.208/tcp/9900/p2p/12D3KooWHZ2G9XscjDGvG7p8uPBoYerDc9kWYnc8oJFGfFxS6gfq",
|
||||
"/ip4/143.198.123.27/tcp/9900/p2p/12D3KooWGzExs5zqebnDvqkUAKaiUuxF3DNbrfJ4prbfkxjXb366",
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/apd/v3"
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/coins"
|
||||
"github.com/athanorlabs/atomic-swap/common/rpctypes"
|
||||
"github.com/athanorlabs/atomic-swap/common/types"
|
||||
contracts "github.com/athanorlabs/atomic-swap/ethereum"
|
||||
"github.com/athanorlabs/atomic-swap/ethereum/extethclient"
|
||||
"github.com/athanorlabs/atomic-swap/monero"
|
||||
"github.com/athanorlabs/atomic-swap/rpcclient"
|
||||
"github.com/athanorlabs/atomic-swap/tests"
|
||||
@@ -21,26 +24,52 @@ import (
|
||||
// Tests the scenario where Bob has XMR and enough ETH to pay gas fees for the token claim. He
|
||||
// exchanges 2 XMR for 3 of Alice's ERC20 tokens.
|
||||
func TestRunSwapDaemon_ExchangesXMRForERC20Tokens(t *testing.T) {
|
||||
minXMR := coins.StrToDecimal("1")
|
||||
maxXMR := coins.StrToDecimal("2")
|
||||
exRate := coins.StrToExchangeRate("1.5")
|
||||
fundingEC := extethclient.CreateTestClient(t, tests.GetTakerTestKey(t))
|
||||
tokenAsset := getMockTetherAsset(t, fundingEC)
|
||||
tokenAddr := tokenAsset.Address()
|
||||
token, err := fundingEC.ERC20Info(context.Background(), tokenAddr)
|
||||
require.NoError(t, err)
|
||||
|
||||
bobConf := CreateTestConf(t, tests.GetMakerTestKey(t))
|
||||
minXMR := coins.StrToDecimal("0.1")
|
||||
maxXMR := coins.StrToDecimal("0.25")
|
||||
exRate := coins.StrToExchangeRate("140")
|
||||
providesAmt := coins.NewEthAssetAmount(coins.StrToDecimal("33.999994"), token) // 33.999994 USDT / 140 = 0.2428571 XMR
|
||||
gasMoney := coins.EtherToWei(coins.StrToDecimal("0.1"))
|
||||
|
||||
bobEthKey, err := crypto.GenerateKey()
|
||||
require.NoError(t, err)
|
||||
bobConf := CreateTestConf(t, bobEthKey)
|
||||
monero.MineMinXMRBalance(t, bobConf.MoneroClient, coins.MoneroToPiconero(maxXMR))
|
||||
|
||||
aliceConf := CreateTestConf(t, tests.GetTakerTestKey(t))
|
||||
|
||||
tokenAsset := getMockTetherAsset(t, aliceConf.EthereumClient)
|
||||
|
||||
// Ensure that Alice has no tokens and definitely no pre-approval to spend
|
||||
// any of those tokens by giving her a brand-new ETH key.
|
||||
aliceEthKey, err := crypto.GenerateKey()
|
||||
require.NoError(t, err)
|
||||
aliceConf := CreateTestConf(t, aliceEthKey)
|
||||
timeout := 7 * time.Minute
|
||||
|
||||
// Fund Alice and Bob with a little ether for gas. Bob needs gas to claim,
|
||||
// as ERC20 token swaps cannot use a relayer.
|
||||
_, err = fundingEC.Transfer(context.Background(), aliceConf.EthereumClient.Address(), gasMoney, nil)
|
||||
require.NoError(t, err)
|
||||
_, err = fundingEC.Transfer(context.Background(), bobConf.EthereumClient.Address(), gasMoney, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Fund Alice with the exact amount of token that she'll provide in the swap
|
||||
// with Bob. After the swap is over, her token balance should be exactly
|
||||
// zero.
|
||||
erc20Iface, err := contracts.NewIERC20(tokenAddr, fundingEC.Raw())
|
||||
require.NoError(t, err)
|
||||
txOpts, err := fundingEC.TxOpts(context.Background())
|
||||
require.NoError(t, err)
|
||||
_, err = erc20Iface.Transfer(txOpts, aliceConf.EthereumClient.Address(), providesAmt.BigInt())
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, _ := LaunchDaemons(t, timeout, aliceConf, bobConf)
|
||||
|
||||
bc := rpcclient.NewClient(ctx, bobConf.RPCPort)
|
||||
ac := rpcclient.NewClient(ctx, aliceConf.RPCPort)
|
||||
|
||||
bobStartTokenBal, err := bobConf.EthereumClient.ERC20Balance(ctx, tokenAsset.Address())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, bobStatusCh, err := bc.MakeOfferAndSubscribe(minXMR, maxXMR, exRate, tokenAsset, false)
|
||||
require.NoError(t, err)
|
||||
time.Sleep(250 * time.Millisecond) // offer propagation time
|
||||
@@ -52,12 +81,9 @@ func TestRunSwapDaemon_ExchangesXMRForERC20Tokens(t *testing.T) {
|
||||
require.Len(t, peersWithOffers[0].Offers, 1)
|
||||
peerID := peersWithOffers[0].PeerID
|
||||
offer := peersWithOffers[0].Offers[0]
|
||||
tokenInfo, err := ac.TokenInfo(offer.EthAsset.Address())
|
||||
require.NoError(t, err)
|
||||
providesAmt, err := exRate.ToERC20Amount(offer.MaxAmount, tokenInfo)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tokenAddr.String(), offer.EthAsset.Address().String())
|
||||
|
||||
aliceStatusCh, err := ac.TakeOfferAndSubscribe(peerID, offer.ID, providesAmt)
|
||||
aliceStatusCh, err := ac.TakeOfferAndSubscribe(peerID, offer.ID, providesAmt.AsStd())
|
||||
require.NoError(t, err)
|
||||
|
||||
var statusWG sync.WaitGroup
|
||||
@@ -105,14 +131,19 @@ func TestRunSwapDaemon_ExchangesXMRForERC20Tokens(t *testing.T) {
|
||||
}
|
||||
|
||||
//
|
||||
// Check Bob's token balance via RPC method instead of doing it directly
|
||||
// Check final token balances via RPC method instead of doing it directly on
|
||||
// the eth client. Bob should have exactly the provided amount and Alice's
|
||||
// token balance should now be zero.
|
||||
//
|
||||
endBalances, err := bc.Balances(&rpctypes.BalancesRequest{TokenAddrs: []ethcommon.Address{tokenAsset.Address()}})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, endBalances.TokenBalances)
|
||||
balReq := &rpctypes.BalancesRequest{TokenAddrs: []ethcommon.Address{tokenAddr}}
|
||||
|
||||
delta := new(apd.Decimal)
|
||||
_, err = coins.DecimalCtx().Sub(delta, endBalances.TokenBalances[0].Amount, bobStartTokenBal.Amount)
|
||||
bobBal, err := bc.Balances(balReq)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, providesAmt.Text('f'), delta.Text('f'))
|
||||
require.NotEmpty(t, bobBal.TokenBalances)
|
||||
require.Equal(t, providesAmt.AsStdString(), bobBal.TokenBalances[0].AsStdString())
|
||||
|
||||
aliceBal, err := ac.Balances(balReq)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, aliceBal.TokenBalances)
|
||||
require.Equal(t, "0", aliceBal.TokenBalances[0].AsStdString())
|
||||
}
|
||||
|
||||
@@ -38,15 +38,15 @@ func TestRecoveryDB_ContractSwapInfo(t *testing.T) {
|
||||
StartNumber: big.NewInt(12345),
|
||||
SwapID: types.Hash{1, 2, 3, 4},
|
||||
Swap: &contracts.SwapCreatorSwap{
|
||||
Owner: ethcommon.HexToAddress("0xda9dfa130df4de4673b89022ee50ff26f6ea73cf"),
|
||||
Claimer: ethcommon.HexToAddress("0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"),
|
||||
PubKeyClaim: ethcommon.HexToHash("0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9"),
|
||||
PubKeyRefund: ethcommon.HexToHash("0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14"),
|
||||
Timeout1: big.NewInt(1672531200),
|
||||
Timeout2: big.NewInt(1672545600),
|
||||
Asset: types.EthAssetETH.Address(),
|
||||
Value: big.NewInt(9876),
|
||||
Nonce: big.NewInt(1234),
|
||||
Owner: ethcommon.HexToAddress("0xda9dfa130df4de4673b89022ee50ff26f6ea73cf"),
|
||||
Claimer: ethcommon.HexToAddress("0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"),
|
||||
ClaimCommitment: ethcommon.HexToHash("0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9"),
|
||||
RefundCommitment: ethcommon.HexToHash("0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14"),
|
||||
Timeout1: big.NewInt(1672531200),
|
||||
Timeout2: big.NewInt(1672545600),
|
||||
Asset: types.EthAssetETH.Address(),
|
||||
Value: big.NewInt(9876),
|
||||
Nonce: big.NewInt(1234),
|
||||
},
|
||||
SwapCreatorAddr: ethcommon.HexToAddress("0xd2b5d6252d0645e4cf4bb547e82a485f527befb7"),
|
||||
}
|
||||
@@ -57,8 +57,8 @@ func TestRecoveryDB_ContractSwapInfo(t *testing.T) {
|
||||
"swap": {
|
||||
"owner": "0xda9dfa130df4de4673b89022ee50ff26f6ea73cf",
|
||||
"claimer": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8",
|
||||
"pubKeyClaim": "0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9",
|
||||
"pubKeyRefund": "0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14",
|
||||
"claimCommitment": "0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9",
|
||||
"refundCommitment": "0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14",
|
||||
"timeout1": 1672531200,
|
||||
"timeout2": 1672545600,
|
||||
"asset": "0x0000000000000000000000000000000000000000",
|
||||
@@ -150,15 +150,15 @@ func TestRecoveryDB_DeleteSwap(t *testing.T) {
|
||||
StartNumber: big.NewInt(12345),
|
||||
SwapID: types.Hash{1, 2, 3, 4},
|
||||
Swap: &contracts.SwapCreatorSwap{
|
||||
Owner: ethcommon.HexToAddress("0xda9dfa130df4de4673b89022ee50ff26f6ea73cf"),
|
||||
Claimer: ethcommon.HexToAddress("0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"),
|
||||
PubKeyClaim: ethcommon.HexToHash("0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9"),
|
||||
PubKeyRefund: ethcommon.HexToHash("0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14"),
|
||||
Timeout1: big.NewInt(1672531200),
|
||||
Timeout2: big.NewInt(1672545600),
|
||||
Asset: types.EthAssetETH.Address(),
|
||||
Value: big.NewInt(9876),
|
||||
Nonce: big.NewInt(1234),
|
||||
Owner: ethcommon.HexToAddress("0xda9dfa130df4de4673b89022ee50ff26f6ea73cf"),
|
||||
Claimer: ethcommon.HexToAddress("0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"),
|
||||
ClaimCommitment: ethcommon.HexToHash("0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9"),
|
||||
RefundCommitment: ethcommon.HexToHash("0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14"),
|
||||
Timeout1: big.NewInt(1672531200),
|
||||
Timeout2: big.NewInt(1672545600),
|
||||
Asset: types.EthAssetETH.Address(),
|
||||
Value: big.NewInt(9876),
|
||||
Nonce: big.NewInt(1234),
|
||||
},
|
||||
SwapCreatorAddr: ethcommon.HexToAddress("0xd2b5d6252d0645e4cf4bb547e82a485f527befb7"),
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ var (
|
||||
// UTContractMetaData contains all meta data concerning the UTContract contract.
|
||||
var UTContractMetaData = &bind.MetaData{
|
||||
ABI: "[{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_stamp\",\"type\":\"uint256\"}],\"name\":\"checkStamp\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]",
|
||||
Bin: "0x608060405234801561001057600080fd5b506101da806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063d5f7a03a14610030575b600080fd5b61004a600480360381019061004591906100d4565b61004c565b005b8042111561008f576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161008690610184565b60405180910390fd5b8060008190555050565b600080fd5b6000819050919050565b6100b18161009e565b81146100bc57600080fd5b50565b6000813590506100ce816100a8565b92915050565b6000602082840312156100ea576100e9610099565b5b60006100f8848285016100bf565b91505092915050565b600082825260208201905092915050565b7f626c6f636b2e74696d657374616d7020776173206e6f74206c6573732074686160008201527f6e207374616d7000000000000000000000000000000000000000000000000000602082015250565b600061016e602783610101565b915061017982610112565b604082019050919050565b6000602082019050818103600083015261019d81610161565b905091905056fea2646970667358221220e3debdc387582462e246390796d566e062d5965fb0bd8de5af573ad815d24b0564736f6c63430008130033",
|
||||
Bin: "0x608060405234801561000f575f80fd5b506101cb8061001d5f395ff3fe608060405234801561000f575f80fd5b5060043610610029575f3560e01c8063d5f7a03a1461002d575b5f80fd5b610047600480360381019061004291906100cc565b610049565b005b8042111561008c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161008390610177565b60405180910390fd5b805f8190555050565b5f80fd5b5f819050919050565b6100ab81610099565b81146100b5575f80fd5b50565b5f813590506100c6816100a2565b92915050565b5f602082840312156100e1576100e0610095565b5b5f6100ee848285016100b8565b91505092915050565b5f82825260208201905092915050565b7f626c6f636b2e74696d657374616d7020776173206e6f74206c657373207468615f8201527f6e207374616d7000000000000000000000000000000000000000000000000000602082015250565b5f6101616027836100f7565b915061016c82610107565b604082019050919050565b5f6020820190508181035f83015261018e81610155565b905091905056fea264697066735822122077057003501b23f47c692e417ddc5f1429b9309ebe23d189b6906de5aa0d260964736f6c63430008140033",
|
||||
}
|
||||
|
||||
// UTContractABI is the input ABI used to generate the binding from.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -5,17 +5,17 @@ package contracts
|
||||
// gas limit. We use these values to estimate minimum required balances.
|
||||
const (
|
||||
MaxNewSwapETHGas = 50639
|
||||
MaxNewSwapTokenGas = 86218
|
||||
MaxNewSwapTokenGas = 87369
|
||||
MaxSetReadyGas = 32054
|
||||
MaxClaimETHGas = 43349
|
||||
MaxClaimTokenGas = 47522
|
||||
MaxClaimTokenGas = 48416
|
||||
MaxRefundETHGas = 43132
|
||||
MaxRefundTokenGas = 47294
|
||||
MaxRefundTokenGas = 48327
|
||||
MaxTokenApproveGas = 47000 // 46223 with our contract
|
||||
)
|
||||
|
||||
// constants that are interesting to track, but not used by swaps
|
||||
const (
|
||||
maxSwapCreatorDeployGas = 1094089
|
||||
maxTestERC20DeployGas = 905727 // using long token names or symbols will increase this
|
||||
maxSwapCreatorDeployGas = 1179944
|
||||
maxTestERC20DeployGas = 932965 // using long token names or symbols will increase this
|
||||
)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC20/ERC20.sol)
|
||||
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/ERC20.sol)
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import {IERC20} from "./IERC20.sol";
|
||||
import {IERC20Metadata} from "./IERC20Metadata.sol";
|
||||
import {Context} from "./Context.sol";
|
||||
import "./IERC20.sol";
|
||||
import "./extensions/IERC20Metadata.sol";
|
||||
import "../../utils/Context.sol";
|
||||
|
||||
/**
|
||||
* @dev Implementation of the {IERC20} interface.
|
||||
@@ -18,6 +18,9 @@ import {Context} from "./Context.sol";
|
||||
* https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How
|
||||
* to implement supply mechanisms].
|
||||
*
|
||||
* The default value of {decimals} is 18. To change this, you should override
|
||||
* this function so it returns a different value.
|
||||
*
|
||||
* We have followed general OpenZeppelin Contracts guidelines: functions revert
|
||||
* instead returning `false` on failure. This behavior is nonetheless
|
||||
* conventional and does not conflict with the expectations of ERC20
|
||||
@@ -45,9 +48,6 @@ contract ERC20 is Context, IERC20, IERC20Metadata {
|
||||
/**
|
||||
* @dev Sets the values for {name} and {symbol}.
|
||||
*
|
||||
* The default value of {decimals} is 18. To select a different value for
|
||||
* {decimals} you should overload it.
|
||||
*
|
||||
* All two of these values are immutable: they can only be set once during
|
||||
* construction.
|
||||
*/
|
||||
@@ -77,8 +77,8 @@ contract ERC20 is Context, IERC20, IERC20Metadata {
|
||||
* be displayed to a user as `5.05` (`505 / 10 ** 2`).
|
||||
*
|
||||
* Tokens usually opt for a value of 18, imitating the relationship between
|
||||
* Ether and Wei. This is the value {ERC20} uses, unless this function is
|
||||
* overridden;
|
||||
* Ether and Wei. This is the default value returned by this function, unless
|
||||
* it's overridden.
|
||||
*
|
||||
* NOTE: This information is only used for _display_ purposes: it in
|
||||
* no way affects any of the arithmetic of the contract, including
|
||||
@@ -119,10 +119,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata {
|
||||
/**
|
||||
* @dev See {IERC20-allowance}.
|
||||
*/
|
||||
function allowance(
|
||||
address owner,
|
||||
address spender
|
||||
) public view virtual override returns (uint256) {
|
||||
function allowance(address owner, address spender) public view virtual override returns (uint256) {
|
||||
return _allowances[owner][spender];
|
||||
}
|
||||
|
||||
@@ -158,11 +155,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata {
|
||||
* - the caller must have allowance for ``from``'s tokens of at least
|
||||
* `amount`.
|
||||
*/
|
||||
function transferFrom(
|
||||
address from,
|
||||
address to,
|
||||
uint256 amount
|
||||
) public virtual override returns (bool) {
|
||||
function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
|
||||
address spender = _msgSender();
|
||||
_spendAllowance(from, spender, amount);
|
||||
_transfer(from, to, amount);
|
||||
@@ -201,10 +194,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata {
|
||||
* - `spender` must have allowance for the caller of at least
|
||||
* `subtractedValue`.
|
||||
*/
|
||||
function decreaseAllowance(
|
||||
address spender,
|
||||
uint256 subtractedValue
|
||||
) public virtual returns (bool) {
|
||||
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
|
||||
address owner = _msgSender();
|
||||
uint256 currentAllowance = allowance(owner, spender);
|
||||
require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
|
||||
@@ -355,7 +345,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata {
|
||||
*
|
||||
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
|
||||
*/
|
||||
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {} // solhint-disable-line
|
||||
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {}
|
||||
|
||||
/**
|
||||
* @dev Hook that is called after any transfer of tokens. This includes
|
||||
@@ -371,5 +361,5 @@ contract ERC20 is Context, IERC20, IERC20Metadata {
|
||||
*
|
||||
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
|
||||
*/
|
||||
function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {} // solhint-disable-line
|
||||
function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol)
|
||||
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol)
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import {IERC20} from "./IERC20.sol";
|
||||
import "../IERC20.sol";
|
||||
|
||||
/**
|
||||
* @dev Interface for the optional metadata functions from the ERC20 standard.
|
||||
@@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/IERC20Permit.sol)
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
/**
|
||||
* @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in
|
||||
* https://eips.ethereum.org/EIPS/eip-2612[EIP-2612].
|
||||
*
|
||||
* Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by
|
||||
* presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't
|
||||
* need to send a transaction, and thus is not required to hold Ether at all.
|
||||
*/
|
||||
interface IERC20Permit {
|
||||
/**
|
||||
* @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens,
|
||||
* given ``owner``'s signed approval.
|
||||
*
|
||||
* IMPORTANT: The same issues {IERC20-approve} has related to transaction
|
||||
* ordering also apply here.
|
||||
*
|
||||
* Emits an {Approval} event.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `spender` cannot be the zero address.
|
||||
* - `deadline` must be a timestamp in the future.
|
||||
* - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner`
|
||||
* over the EIP712-formatted function arguments.
|
||||
* - the signature must use ``owner``'s current nonce (see {nonces}).
|
||||
*
|
||||
* For more information on the signature format, see the
|
||||
* https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP
|
||||
* section].
|
||||
*/
|
||||
function permit(
|
||||
address owner,
|
||||
address spender,
|
||||
uint256 value,
|
||||
uint256 deadline,
|
||||
uint8 v,
|
||||
bytes32 r,
|
||||
bytes32 s
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @dev Returns the current nonce for `owner`. This value must be
|
||||
* included whenever a signature is generated for {permit}.
|
||||
*
|
||||
* Every successful call to {permit} increases ``owner``'s nonce by one. This
|
||||
* prevents a signature from being used multiple times.
|
||||
*/
|
||||
function nonces(address owner) external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}.
|
||||
*/
|
||||
// solhint-disable-next-line func-name-mixedcase
|
||||
function DOMAIN_SEPARATOR() external view returns (bytes32);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/utils/SafeERC20.sol)
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "../IERC20.sol";
|
||||
import "../extensions/IERC20Permit.sol";
|
||||
import "../../../utils/Address.sol";
|
||||
|
||||
/**
|
||||
* @title SafeERC20
|
||||
* @dev Wrappers around ERC20 operations that throw on failure (when the token
|
||||
* contract returns false). Tokens that return no value (and instead revert or
|
||||
* throw on failure) are also supported, non-reverting calls are assumed to be
|
||||
* successful.
|
||||
* To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
|
||||
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
|
||||
*/
|
||||
library SafeERC20 {
|
||||
using Address for address;
|
||||
|
||||
/**
|
||||
* @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
|
||||
* non-reverting calls are assumed to be successful.
|
||||
*/
|
||||
function safeTransfer(IERC20 token, address to, uint256 value) internal {
|
||||
_callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
|
||||
* calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
|
||||
*/
|
||||
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
|
||||
_callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Deprecated. This function has issues similar to the ones found in
|
||||
* {IERC20-approve}, and its usage is discouraged.
|
||||
*
|
||||
* Whenever possible, use {safeIncreaseAllowance} and
|
||||
* {safeDecreaseAllowance} instead.
|
||||
*/
|
||||
function safeApprove(IERC20 token, address spender, uint256 value) internal {
|
||||
// safeApprove should only be called when setting an initial allowance,
|
||||
// or when resetting it to zero. To increase and decrease it, use
|
||||
// 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
|
||||
require(
|
||||
(value == 0) || (token.allowance(address(this), spender) == 0),
|
||||
"SafeERC20: approve from non-zero to non-zero allowance"
|
||||
);
|
||||
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
|
||||
* non-reverting calls are assumed to be successful.
|
||||
*/
|
||||
function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
|
||||
uint256 oldAllowance = token.allowance(address(this), spender);
|
||||
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance + value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Decrease the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
|
||||
* non-reverting calls are assumed to be successful.
|
||||
*/
|
||||
function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal {
|
||||
unchecked {
|
||||
uint256 oldAllowance = token.allowance(address(this), spender);
|
||||
require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
|
||||
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance - value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
|
||||
* non-reverting calls are assumed to be successful. Compatible with tokens that require the approval to be set to
|
||||
* 0 before setting it to a non-zero value.
|
||||
*/
|
||||
function forceApprove(IERC20 token, address spender, uint256 value) internal {
|
||||
bytes memory approvalCall = abi.encodeWithSelector(token.approve.selector, spender, value);
|
||||
|
||||
if (!_callOptionalReturnBool(token, approvalCall)) {
|
||||
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, 0));
|
||||
_callOptionalReturn(token, approvalCall);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Use a ERC-2612 signature to set the `owner` approval toward `spender` on `token`.
|
||||
* Revert on invalid signature.
|
||||
*/
|
||||
function safePermit(
|
||||
IERC20Permit token,
|
||||
address owner,
|
||||
address spender,
|
||||
uint256 value,
|
||||
uint256 deadline,
|
||||
uint8 v,
|
||||
bytes32 r,
|
||||
bytes32 s
|
||||
) internal {
|
||||
uint256 nonceBefore = token.nonces(owner);
|
||||
token.permit(owner, spender, value, deadline, v, r, s);
|
||||
uint256 nonceAfter = token.nonces(owner);
|
||||
require(nonceAfter == nonceBefore + 1, "SafeERC20: permit did not succeed");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
|
||||
* on the return value: the return value is optional (but if data is returned, it must not be false).
|
||||
* @param token The token targeted by the call.
|
||||
* @param data The call data (encoded using abi.encode or one of its variants).
|
||||
*/
|
||||
function _callOptionalReturn(IERC20 token, bytes memory data) private {
|
||||
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
|
||||
// we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that
|
||||
// the target address contains contract code and also asserts for success in the low-level call.
|
||||
|
||||
bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
|
||||
require(returndata.length == 0 || abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
|
||||
* on the return value: the return value is optional (but if data is returned, it must not be false).
|
||||
* @param token The token targeted by the call.
|
||||
* @param data The call data (encoded using abi.encode or one of its variants).
|
||||
*
|
||||
* This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead.
|
||||
*/
|
||||
function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
|
||||
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
|
||||
// we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false
|
||||
// and not revert is the subcall reverts.
|
||||
|
||||
(bool success, bytes memory returndata) = address(token).call(data);
|
||||
return
|
||||
success && (returndata.length == 0 || abi.decode(returndata, (bool))) && Address.isContract(address(token));
|
||||
}
|
||||
}
|
||||
244
ethereum/contracts/@openzeppelin/contracts/utils/Address.sol
Normal file
244
ethereum/contracts/@openzeppelin/contracts/utils/Address.sol
Normal file
@@ -0,0 +1,244 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts (last updated v4.9.0) (utils/Address.sol)
|
||||
|
||||
pragma solidity ^0.8.1;
|
||||
|
||||
/**
|
||||
* @dev Collection of functions related to the address type
|
||||
*/
|
||||
library Address {
|
||||
/**
|
||||
* @dev Returns true if `account` is a contract.
|
||||
*
|
||||
* [IMPORTANT]
|
||||
* ====
|
||||
* It is unsafe to assume that an address for which this function returns
|
||||
* false is an externally-owned account (EOA) and not a contract.
|
||||
*
|
||||
* Among others, `isContract` will return false for the following
|
||||
* types of addresses:
|
||||
*
|
||||
* - an externally-owned account
|
||||
* - a contract in construction
|
||||
* - an address where a contract will be created
|
||||
* - an address where a contract lived, but was destroyed
|
||||
*
|
||||
* Furthermore, `isContract` will also return true if the target contract within
|
||||
* the same transaction is already scheduled for destruction by `SELFDESTRUCT`,
|
||||
* which only has an effect at the end of a transaction.
|
||||
* ====
|
||||
*
|
||||
* [IMPORTANT]
|
||||
* ====
|
||||
* You shouldn't rely on `isContract` to protect against flash loan attacks!
|
||||
*
|
||||
* Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets
|
||||
* like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract
|
||||
* constructor.
|
||||
* ====
|
||||
*/
|
||||
function isContract(address account) internal view returns (bool) {
|
||||
// This method relies on extcodesize/address.code.length, which returns 0
|
||||
// for contracts in construction, since the code is only stored at the end
|
||||
// of the constructor execution.
|
||||
|
||||
return account.code.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Replacement for Solidity's `transfer`: sends `amount` wei to
|
||||
* `recipient`, forwarding all available gas and reverting on errors.
|
||||
*
|
||||
* https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
|
||||
* of certain opcodes, possibly making contracts go over the 2300 gas limit
|
||||
* imposed by `transfer`, making them unable to receive funds via
|
||||
* `transfer`. {sendValue} removes this limitation.
|
||||
*
|
||||
* https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more].
|
||||
*
|
||||
* IMPORTANT: because control is transferred to `recipient`, care must be
|
||||
* taken to not create reentrancy vulnerabilities. Consider using
|
||||
* {ReentrancyGuard} or the
|
||||
* https://solidity.readthedocs.io/en/v0.8.0/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
|
||||
*/
|
||||
function sendValue(address payable recipient, uint256 amount) internal {
|
||||
require(address(this).balance >= amount, "Address: insufficient balance");
|
||||
|
||||
(bool success, ) = recipient.call{value: amount}("");
|
||||
require(success, "Address: unable to send value, recipient may have reverted");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Performs a Solidity function call using a low level `call`. A
|
||||
* plain `call` is an unsafe replacement for a function call: use this
|
||||
* function instead.
|
||||
*
|
||||
* If `target` reverts with a revert reason, it is bubbled up by this
|
||||
* function (like regular Solidity function calls).
|
||||
*
|
||||
* Returns the raw returned data. To convert to the expected return value,
|
||||
* use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `target` must be a contract.
|
||||
* - calling `target` with `data` must not revert.
|
||||
*
|
||||
* _Available since v3.1._
|
||||
*/
|
||||
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
|
||||
return functionCallWithValue(target, data, 0, "Address: low-level call failed");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
|
||||
* `errorMessage` as a fallback revert reason when `target` reverts.
|
||||
*
|
||||
* _Available since v3.1._
|
||||
*/
|
||||
function functionCall(
|
||||
address target,
|
||||
bytes memory data,
|
||||
string memory errorMessage
|
||||
) internal returns (bytes memory) {
|
||||
return functionCallWithValue(target, data, 0, errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
|
||||
* but also transferring `value` wei to `target`.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - the calling contract must have an ETH balance of at least `value`.
|
||||
* - the called Solidity function must be `payable`.
|
||||
*
|
||||
* _Available since v3.1._
|
||||
*/
|
||||
function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
|
||||
return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
|
||||
* with `errorMessage` as a fallback revert reason when `target` reverts.
|
||||
*
|
||||
* _Available since v3.1._
|
||||
*/
|
||||
function functionCallWithValue(
|
||||
address target,
|
||||
bytes memory data,
|
||||
uint256 value,
|
||||
string memory errorMessage
|
||||
) internal returns (bytes memory) {
|
||||
require(address(this).balance >= value, "Address: insufficient balance for call");
|
||||
(bool success, bytes memory returndata) = target.call{value: value}(data);
|
||||
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
|
||||
* but performing a static call.
|
||||
*
|
||||
* _Available since v3.3._
|
||||
*/
|
||||
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
|
||||
return functionStaticCall(target, data, "Address: low-level static call failed");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
|
||||
* but performing a static call.
|
||||
*
|
||||
* _Available since v3.3._
|
||||
*/
|
||||
function functionStaticCall(
|
||||
address target,
|
||||
bytes memory data,
|
||||
string memory errorMessage
|
||||
) internal view returns (bytes memory) {
|
||||
(bool success, bytes memory returndata) = target.staticcall(data);
|
||||
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
|
||||
* but performing a delegate call.
|
||||
*
|
||||
* _Available since v3.4._
|
||||
*/
|
||||
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
|
||||
return functionDelegateCall(target, data, "Address: low-level delegate call failed");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
|
||||
* but performing a delegate call.
|
||||
*
|
||||
* _Available since v3.4._
|
||||
*/
|
||||
function functionDelegateCall(
|
||||
address target,
|
||||
bytes memory data,
|
||||
string memory errorMessage
|
||||
) internal returns (bytes memory) {
|
||||
(bool success, bytes memory returndata) = target.delegatecall(data);
|
||||
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling
|
||||
* the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract.
|
||||
*
|
||||
* _Available since v4.8._
|
||||
*/
|
||||
function verifyCallResultFromTarget(
|
||||
address target,
|
||||
bool success,
|
||||
bytes memory returndata,
|
||||
string memory errorMessage
|
||||
) internal view returns (bytes memory) {
|
||||
if (success) {
|
||||
if (returndata.length == 0) {
|
||||
// only check isContract if the call was successful and the return data is empty
|
||||
// otherwise we already know that it was a contract
|
||||
require(isContract(target), "Address: call to non-contract");
|
||||
}
|
||||
return returndata;
|
||||
} else {
|
||||
_revert(returndata, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the
|
||||
* revert reason or using the provided one.
|
||||
*
|
||||
* _Available since v4.3._
|
||||
*/
|
||||
function verifyCallResult(
|
||||
bool success,
|
||||
bytes memory returndata,
|
||||
string memory errorMessage
|
||||
) internal pure returns (bytes memory) {
|
||||
if (success) {
|
||||
return returndata;
|
||||
} else {
|
||||
_revert(returndata, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
function _revert(bytes memory returndata, string memory errorMessage) private pure {
|
||||
// Look for revert reason and bubble it up if present
|
||||
if (returndata.length > 0) {
|
||||
// The easiest way to bubble the revert reason is using memory via assembly
|
||||
/// @solidity memory-safe-assembly
|
||||
assembly {
|
||||
let returndata_size := mload(returndata)
|
||||
revert(add(32, returndata), returndata_size)
|
||||
}
|
||||
} else {
|
||||
revert(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)
|
||||
|
||||
pragma solidity ^0.8.19;
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
/**
|
||||
* @dev Provides information about the current execution context, including the
|
||||
@@ -17,4 +17,8 @@ abstract contract Context {
|
||||
function _msgSender() internal view virtual returns (address) {
|
||||
return msg.sender;
|
||||
}
|
||||
|
||||
function _msgData() internal view virtual returns (bytes calldata) {
|
||||
return msg.data;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,23 @@
|
||||
// SPDX-License-Identifier: LGPLv3
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {IERC20} from "./IERC20.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import {Secp256k1} from "./Secp256k1.sol";
|
||||
|
||||
// SwapCreator facilitates swapping between Alice, a party that has an EVM
|
||||
// native currency or a token (ERC-20 or compatible API) that she wants to
|
||||
// exchange cross-chain for a different currency, and Bob, a party that has the
|
||||
// other chain's currency and wishes to exchange it for Alice's currency.
|
||||
contract SwapCreator is Secp256k1 {
|
||||
// Swap state is PENDING when the swap is first created and funded
|
||||
// Alice sets Stage to READY when she sees the funds locked on the other chain.
|
||||
// this prevents Bob from withdrawing funds without locking funds on the other chain first
|
||||
// Stage is set to COMPLETED upon the swap value being claimed or refunded.
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
// Stage represents the swap state. It is PENDING when `newSwap` is called
|
||||
// to create and fund the swap. Alice sets Stage to READY, via `setReady`,
|
||||
// after verifying that funds are locked on the other chain. Bob cannot
|
||||
// claim the swap funds until Alice sets the swap Stage to READY. The Stage
|
||||
// is set to COMPLETED when Bob claims directly via `claim` or indirectly
|
||||
// via `claimRelayer`, or by Alice calling `refund`.
|
||||
enum Stage {
|
||||
INVALID,
|
||||
PENDING,
|
||||
@@ -16,46 +25,55 @@ contract SwapCreator is Secp256k1 {
|
||||
COMPLETED
|
||||
}
|
||||
|
||||
// swaps maps from a swap ID to the swap's current Stage
|
||||
mapping(bytes32 => Stage) public swaps;
|
||||
|
||||
// Swap stores the swap parameters, the hash of which forms the swap ID.
|
||||
struct Swap {
|
||||
// the swap initiator, Alice
|
||||
// address allowed to refund the ether for this swap
|
||||
// owner is the address of Alice, who initiates the swap by calling
|
||||
// `newSwap`. Only the owner is allowed to call `setReady` or `refund`.
|
||||
address payable owner;
|
||||
// address allowed to claim the ether for this swap, Bob
|
||||
// claimer is the address of Bob. Only the claimer can call `claim` or
|
||||
// sign a RelaySwap object that `claimRelayer` will accept the signature
|
||||
// for.
|
||||
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 before which Alice can call either `setReady` or `refund`
|
||||
// claimCommitment is the Keccak-256 hash of the expected secp256k1
|
||||
// public key derived from the secret (private key) that Bob sends when
|
||||
// claiming. Alice receives this commitment off-chain.
|
||||
bytes32 claimCommitment;
|
||||
// refundCommitment is the Keccak-256 hash of the expected secp256k1
|
||||
// public key derived from the secret (private key) that Alice sends if
|
||||
// refunding.
|
||||
bytes32 refundCommitment;
|
||||
// timeout1 is the block timestamp before which Alice can call
|
||||
// either `setReady` or `refund`.
|
||||
uint256 timeout1;
|
||||
// timestamp after which Bob cannot claim, only Alice can refund
|
||||
// timeout2 is the block timestamp after which Bob cannot claim, only
|
||||
// Alice can refund.
|
||||
uint256 timeout2;
|
||||
// the asset being swapped: equal to address(0) for ETH, or an ERC-20 token address
|
||||
// asset is address(0) for EVM native currency swaps, or it is the
|
||||
// address of the token that Alice is providing.
|
||||
address asset;
|
||||
// the value of this swap
|
||||
// value is the wei or token unit amount that Alice locked in the contract
|
||||
uint256 value;
|
||||
// choose random
|
||||
// nonce is a random value chosen by Alice
|
||||
uint256 nonce;
|
||||
}
|
||||
|
||||
// RelaySwap contains additional information required for relayed transactions.
|
||||
// This entire structure is encoded and signed by the swap claimer, and the signature is
|
||||
// passed to the `claimRelayer` function.
|
||||
// RelaySwap contains additional information required for relayed claim
|
||||
// transactions. This entire structure is encoded and signed by the swap
|
||||
// claimer, and the signature is passed to `claimRelayer`.
|
||||
struct RelaySwap {
|
||||
// the swap the transaction is for
|
||||
// swap specifies which swap is being claimed
|
||||
Swap swap;
|
||||
// the fee, in wei, paid to the relayer
|
||||
// fee is the wei amount paid to the relayer
|
||||
uint256 fee;
|
||||
// hash of (relayer's payout address || 4-byte salt)
|
||||
// relayerHash Keccak-256 hash of (relayer's payout address || 4-byte salt)
|
||||
bytes32 relayerHash;
|
||||
// address of the swap contract this transaction is meant for
|
||||
// swapCreator is the address of the swap's contract
|
||||
address swapCreator;
|
||||
}
|
||||
|
||||
mapping(bytes32 => Stage) public swaps;
|
||||
|
||||
event New(
|
||||
bytes32 swapID,
|
||||
bytes32 claimKey,
|
||||
@@ -69,68 +87,86 @@ contract SwapCreator is Secp256k1 {
|
||||
event Claimed(bytes32 indexed swapID, bytes32 indexed s);
|
||||
event Refunded(bytes32 indexed swapID, bytes32 indexed s);
|
||||
|
||||
// returned when trying to initiate a swap with a zero value
|
||||
// thrown when the value parameter to `newSwap` is zero
|
||||
error ZeroValue();
|
||||
|
||||
// returned when the pubKeyClaim or pubKeyRefund parameters for `newSwap` are zero
|
||||
// thrown when either of the claimCommitment or refundCommitment parameters
|
||||
// passed to `newSwap` are zero
|
||||
error InvalidSwapKey();
|
||||
|
||||
// returned when the claimer parameter for `newSwap` is the zero address
|
||||
// thrown when the claimer parameter for `newSwap` is the zero address
|
||||
error InvalidClaimer();
|
||||
|
||||
// returned when the timeout1 or timeout2 parameters for `newSwap` are zero
|
||||
// thrown when the timeout1 or timeout2 parameters for `newSwap` are zero
|
||||
error InvalidTimeout();
|
||||
|
||||
// returned when the ether sent with a `newSwap` transaction does not match the value parameter
|
||||
// thrown when msg.value of a `newSwap` transaction has the wrong value
|
||||
error InvalidValue();
|
||||
|
||||
// returned when trying to initiate a swap with an ID that already exists
|
||||
// thrown when trying to initiate a swap with an ID that already exists
|
||||
error SwapAlreadyExists();
|
||||
|
||||
// returned when trying to call `setReady` on a swap that is not in the PENDING stage
|
||||
// thrown when trying to call `setReady` on a swap that is not in the
|
||||
// PENDING stage
|
||||
error SwapNotPending();
|
||||
|
||||
// returned when the caller of `setReady` or `refund` is not the swap owner
|
||||
// thrown when the caller of `setReady` or `refund` is not the swap owner
|
||||
error OnlySwapOwner();
|
||||
|
||||
// returned when the signer of the relayed transaction is not the swap's claimer
|
||||
// thrown when the signer of the relayed transaction is not the swap's
|
||||
// claimer
|
||||
error OnlySwapClaimer();
|
||||
|
||||
// returned when trying to call `claim` or `refund` on an invalid swap
|
||||
// thrown when trying to call `claim` or `refund` on an invalid swap
|
||||
error InvalidSwap();
|
||||
|
||||
// returned when trying to call `claim` or `refund` on a swap that's already completed
|
||||
// thrown when trying to call `claim` or `refund` on a swap that's already
|
||||
// completed
|
||||
error SwapCompleted();
|
||||
|
||||
// returned when trying to call `claim` on a swap that's not set to ready or the first timeout has not been reached
|
||||
// thrown when trying to call `claim` on a swap that's not set to ready or
|
||||
// the first timeout has not been reached
|
||||
error TooEarlyToClaim();
|
||||
|
||||
// returned when trying to call `claim` on a swap where the second timeout has been reached
|
||||
// thrown when trying to call `claim` on a swap where the second timeout has
|
||||
// been reached
|
||||
error TooLateToClaim();
|
||||
|
||||
// returned when it's the counterparty's turn to claim and refunding is not allowed
|
||||
// thrown when it's the counterparty's turn to claim and refunding is not
|
||||
// allowed
|
||||
error NotTimeToRefund();
|
||||
|
||||
// returned when the provided secret does not match the expected public key
|
||||
// thrown when the provided secret does not match its expected public key
|
||||
// hash
|
||||
error InvalidSecret();
|
||||
|
||||
// returned when the signature of a `RelaySwap` is invalid
|
||||
// thrown when the signature of a `RelaySwap` is invalid
|
||||
error InvalidSignature();
|
||||
|
||||
// returned when the SwapCreator address is a `RelaySwap` is not the addres of this contract
|
||||
// thrown when the SwapCreator address is a `RelaySwap` is not the address
|
||||
// of this contract
|
||||
error InvalidContractAddress();
|
||||
|
||||
// returned when the hash of the relayer address and salt passed to `claimRelayer`
|
||||
// does not match the relayer hash in `RelaySwap`
|
||||
// thrown when the hash of the relayer address and salt passed to
|
||||
// `claimRelayer` does not match the relayer hash in `RelaySwap`
|
||||
error InvalidRelayerAddress();
|
||||
|
||||
// newSwap creates a new Swap instance with the given parameters.
|
||||
// it returns the swap's ID.
|
||||
// _timeoutDuration0: duration between the current timestamp and timeout1
|
||||
// _timeoutDuration1: duration between timeout1 and timeout2
|
||||
// `newSwap` creates a new Swap instance using the passed parameters and
|
||||
// locks Alice's native EVM currency or token asset in the contract. On
|
||||
// success, the swap ID is returned.
|
||||
//
|
||||
// Note that the duration values are distinct from the timeout values:
|
||||
//
|
||||
// _timeoutDuration1:
|
||||
// duration, in seconds, between the current block timestamp and
|
||||
// timeout1
|
||||
//
|
||||
// _timeoutDuration2:
|
||||
// duration, in seconds, between timeout1 and timeout2
|
||||
//
|
||||
function newSwap(
|
||||
bytes32 _pubKeyClaim,
|
||||
bytes32 _pubKeyRefund,
|
||||
bytes32 _claimCommitment,
|
||||
bytes32 _refundCommitment,
|
||||
address payable _claimer,
|
||||
uint256 _timeoutDuration1,
|
||||
uint256 _timeoutDuration2,
|
||||
@@ -142,19 +178,19 @@ contract SwapCreator is Secp256k1 {
|
||||
if (_asset == address(0)) {
|
||||
if (_value != msg.value) revert InvalidValue();
|
||||
} else {
|
||||
// transfer ERC-20 token into this contract
|
||||
// transfer the token amount to this contract
|
||||
// WARN: fee-on-transfer tokens are not supported
|
||||
IERC20(_asset).transferFrom(msg.sender, address(this), _value);
|
||||
IERC20(_asset).safeTransferFrom(msg.sender, address(this), _value);
|
||||
}
|
||||
|
||||
if (_pubKeyClaim == 0 || _pubKeyRefund == 0) revert InvalidSwapKey();
|
||||
if (_claimCommitment == 0 || _refundCommitment == 0) revert InvalidSwapKey();
|
||||
if (_claimer == address(0)) revert InvalidClaimer();
|
||||
if (_timeoutDuration1 == 0 || _timeoutDuration2 == 0) revert InvalidTimeout();
|
||||
|
||||
Swap memory swap = Swap({
|
||||
owner: payable(msg.sender),
|
||||
pubKeyClaim: _pubKeyClaim,
|
||||
pubKeyRefund: _pubKeyRefund,
|
||||
claimCommitment: _claimCommitment,
|
||||
refundCommitment: _refundCommitment,
|
||||
claimer: _claimer,
|
||||
timeout1: block.timestamp + _timeoutDuration1,
|
||||
timeout2: block.timestamp + _timeoutDuration1 + _timeoutDuration2,
|
||||
@@ -165,13 +201,13 @@ contract SwapCreator is Secp256k1 {
|
||||
|
||||
bytes32 swapID = keccak256(abi.encode(swap));
|
||||
|
||||
// make sure this isn't overriding an existing swap
|
||||
// ensure that we are not overriding an existing swap
|
||||
if (swaps[swapID] != Stage.INVALID) revert SwapAlreadyExists();
|
||||
|
||||
emit New(
|
||||
swapID,
|
||||
_pubKeyClaim,
|
||||
_pubKeyRefund,
|
||||
_claimCommitment,
|
||||
_refundCommitment,
|
||||
swap.timeout1,
|
||||
swap.timeout2,
|
||||
swap.asset,
|
||||
@@ -181,7 +217,8 @@ contract SwapCreator is Secp256k1 {
|
||||
return swapID;
|
||||
}
|
||||
|
||||
// Alice should call setReady() before timeout1 once she verifies the XMR has been locked
|
||||
// Alice should call `setReady` before timeout1 and after verifying that Bob
|
||||
// locked his swap funds.
|
||||
function setReady(Swap memory _swap) public {
|
||||
bytes32 swapID = keccak256(abi.encode(_swap));
|
||||
if (swaps[swapID] != Stage.PENDING) revert SwapNotPending();
|
||||
@@ -190,26 +227,28 @@ contract SwapCreator is Secp256k1 {
|
||||
emit Ready(swapID);
|
||||
}
|
||||
|
||||
// Bob can call claim if either of these hold true:
|
||||
// Bob can call `claim` if either of these hold true:
|
||||
// (1) Alice has set the swap to `ready` and it's before timeout1
|
||||
// (2) It is between timeout0 and timeout1
|
||||
// (2) It is between timeout1 and timeout2
|
||||
function claim(Swap memory _swap, bytes32 _secret) public {
|
||||
if (msg.sender != _swap.claimer) revert OnlySwapClaimer();
|
||||
_claim(_swap, _secret);
|
||||
|
||||
// send ether to swap claimer
|
||||
if (_swap.asset == address(0)) {
|
||||
// Transfer the swap value as the EVM's native currency
|
||||
_swap.claimer.transfer(_swap.value);
|
||||
} else {
|
||||
// WARN: this will FAIL for fee-on-transfer or rebasing tokens if the token
|
||||
// transfer reverts (i.e. if this contract does not contain _swap.value tokens),
|
||||
// exposing Bob's secret while giving him nothing.
|
||||
IERC20(_swap.asset).transfer(_swap.claimer, _swap.value);
|
||||
// Transfer the swap value as a token amount.
|
||||
// WARNING: this will FAIL for fee-on-transfer or rebasing tokens if
|
||||
// the token transfer reverts (i.e. if this contract does not
|
||||
// contain _swap.value tokens), exposing Bob's secret while giving
|
||||
// him nothing.
|
||||
IERC20(_swap.asset).safeTransfer(_swap.claimer, _swap.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Anyone can call claimRelayer if they receive a signed _relaySwap object
|
||||
// from Bob. The same rules for when Bob can call claim() apply here when a
|
||||
// Anyone can call `claimRelayer` if they receive a signed _relaySwap object
|
||||
// from Bob. The same rules for when Bob can call `claim` apply here when a
|
||||
// 3rd party relays a claim for Bob. This version of claiming transfers a
|
||||
// _relaySwap.fee to _relayer. To prevent front-running, while not requiring
|
||||
// Bob to know the relayer's payout address, Bob only signs a salted hash of
|
||||
@@ -241,11 +280,11 @@ contract SwapCreator is Secp256k1 {
|
||||
// WARN: this will FAIL for fee-on-transfer or rebasing tokens if the token
|
||||
// transfer reverts (i.e. if this contract does not contain _swap.value tokens),
|
||||
// exposing Bob's secret while giving him nothing.
|
||||
IERC20(_relaySwap.swap.asset).transfer(
|
||||
IERC20(_relaySwap.swap.asset).safeTransfer(
|
||||
_relaySwap.swap.claimer,
|
||||
_relaySwap.swap.value - _relaySwap.fee
|
||||
);
|
||||
IERC20(_relaySwap.swap.asset).transfer(_relayer, _relaySwap.fee);
|
||||
IERC20(_relaySwap.swap.asset).safeTransfer(_relayer, _relaySwap.fee);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,14 +296,14 @@ contract SwapCreator is Secp256k1 {
|
||||
if (block.timestamp < _swap.timeout1 && swapStage != Stage.READY) revert TooEarlyToClaim();
|
||||
if (block.timestamp >= _swap.timeout2) revert TooLateToClaim();
|
||||
|
||||
verifySecret(_secret, _swap.pubKeyClaim);
|
||||
verifySecret(_secret, _swap.claimCommitment);
|
||||
emit Claimed(swapID, _secret);
|
||||
swaps[swapID] = Stage.COMPLETED;
|
||||
}
|
||||
|
||||
// Alice can claim a refund:
|
||||
// - Until timeout1 unless she calls setReady
|
||||
// - After timeout2
|
||||
// Alice can `refund` her swap funds:
|
||||
// - Until timeout1, unless she called `setReady`
|
||||
// - After timeout2, independent of whether she called `setReady`
|
||||
function refund(Swap memory _swap, bytes32 _secret) public {
|
||||
bytes32 swapID = keccak256(abi.encode(_swap));
|
||||
Stage swapStage = swaps[swapID];
|
||||
@@ -276,7 +315,7 @@ contract SwapCreator is Secp256k1 {
|
||||
(block.timestamp > _swap.timeout1 || swapStage == Stage.READY)
|
||||
) revert NotTimeToRefund();
|
||||
|
||||
verifySecret(_secret, _swap.pubKeyRefund);
|
||||
verifySecret(_secret, _swap.refundCommitment);
|
||||
emit Refunded(swapID, _secret);
|
||||
|
||||
// send asset back to swap owner
|
||||
@@ -284,7 +323,7 @@ contract SwapCreator is Secp256k1 {
|
||||
if (_swap.asset == address(0)) {
|
||||
_swap.owner.transfer(_swap.value);
|
||||
} else {
|
||||
IERC20(_swap.asset).transfer(_swap.owner, _swap.value);
|
||||
IERC20(_swap.asset).safeTransfer(_swap.owner, _swap.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import {ERC20} from "./ERC20.sol";
|
||||
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
|
||||
// ERC20 token for testing purposes
|
||||
contract TestERC20 is ERC20 {
|
||||
@@ -31,6 +31,25 @@ contract TestERC20 is ERC20 {
|
||||
_burn(account, amount);
|
||||
}
|
||||
|
||||
function approve(address spender, uint256 amount) public virtual override returns (bool) {
|
||||
address owner = _msgSender();
|
||||
|
||||
// This next checks is performed by the USDT contract, that we want to
|
||||
// be compatible with:
|
||||
// https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7#code
|
||||
//
|
||||
// To change the approve amount you first have to reduce the addresses
|
||||
// allowance to zero to prevent an attack described here:
|
||||
// https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/edit
|
||||
require(
|
||||
amount == 0 || allowance(owner, spender) == 0,
|
||||
"approve allowance must be set to zero before updating"
|
||||
);
|
||||
|
||||
_approve(owner, spender, amount);
|
||||
return true;
|
||||
}
|
||||
|
||||
function transferInternal(address from, address to, uint256 value) public {
|
||||
_transfer(from, to, value);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -301,6 +301,14 @@ func (c *ethClient) LatestBlockTimestamp(ctx context.Context) (time.Time, error)
|
||||
return time.Unix(int64(hdr.Time), 0), nil
|
||||
}
|
||||
|
||||
// Lock is used for 2 purposes:
|
||||
// 1. Nonce synchronization: Any time you do a transaction, the lock should be
|
||||
// grabbed before sending the transaction to the mempool and not released until
|
||||
// after the transaction has been mined into a block (receipt is returned).
|
||||
// 2. Transactions that must be done together atomically. In our case, the token
|
||||
// approve(...) call and TransferFrom(...) call made by the SwapCreator
|
||||
// contract must be performed as one atomic unit without another `approve` call
|
||||
// made in the middle.
|
||||
func (c *ethClient) Lock() {
|
||||
c.mu.Lock()
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -15,29 +15,29 @@ import (
|
||||
// swap is the same as the auto-generated SwapCreatorSwap type, but with some type
|
||||
// adjustments and annotations for JSON marshalling.
|
||||
type swap struct {
|
||||
Owner common.Address `json:"owner" validate:"required"`
|
||||
Claimer common.Address `json:"claimer" validate:"required"`
|
||||
PubKeyClaim types.Hash `json:"pubKeyClaim" validate:"required"`
|
||||
PubKeyRefund types.Hash `json:"pubKeyRefund" validate:"required"`
|
||||
Timeout1 *big.Int `json:"timeout1" validate:"required"`
|
||||
Timeout2 *big.Int `json:"timeout2" validate:"required"`
|
||||
Asset common.Address `json:"asset"`
|
||||
Value *big.Int `json:"value" validate:"required"`
|
||||
Nonce *big.Int `json:"nonce" validate:"required"`
|
||||
Owner common.Address `json:"owner" validate:"required"`
|
||||
Claimer common.Address `json:"claimer" validate:"required"`
|
||||
ClaimCommitment types.Hash `json:"claimCommitment" validate:"required"`
|
||||
RefundCommitment types.Hash `json:"refundCommitment" validate:"required"`
|
||||
Timeout1 *big.Int `json:"timeout1" validate:"required"`
|
||||
Timeout2 *big.Int `json:"timeout2" validate:"required"`
|
||||
Asset common.Address `json:"asset"`
|
||||
Value *big.Int `json:"value" validate:"required"`
|
||||
Nonce *big.Int `json:"nonce" validate:"required"`
|
||||
}
|
||||
|
||||
// MarshalJSON provides JSON marshalling for SwapCreatorSwap
|
||||
func (sfs *SwapCreatorSwap) MarshalJSON() ([]byte, error) {
|
||||
return vjson.MarshalStruct(&swap{
|
||||
Owner: sfs.Owner,
|
||||
Claimer: sfs.Claimer,
|
||||
PubKeyClaim: sfs.PubKeyClaim,
|
||||
PubKeyRefund: sfs.PubKeyRefund,
|
||||
Timeout1: sfs.Timeout1,
|
||||
Timeout2: sfs.Timeout2,
|
||||
Asset: sfs.Asset,
|
||||
Value: sfs.Value,
|
||||
Nonce: sfs.Nonce,
|
||||
Owner: sfs.Owner,
|
||||
Claimer: sfs.Claimer,
|
||||
ClaimCommitment: sfs.ClaimCommitment,
|
||||
RefundCommitment: sfs.RefundCommitment,
|
||||
Timeout1: sfs.Timeout1,
|
||||
Timeout2: sfs.Timeout2,
|
||||
Asset: sfs.Asset,
|
||||
Value: sfs.Value,
|
||||
Nonce: sfs.Nonce,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -48,15 +48,15 @@ func (sfs *SwapCreatorSwap) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
*sfs = SwapCreatorSwap{
|
||||
Owner: s.Owner,
|
||||
Claimer: s.Claimer,
|
||||
PubKeyClaim: s.PubKeyClaim,
|
||||
PubKeyRefund: s.PubKeyRefund,
|
||||
Timeout1: s.Timeout1,
|
||||
Timeout2: s.Timeout2,
|
||||
Asset: s.Asset,
|
||||
Value: s.Value,
|
||||
Nonce: s.Nonce,
|
||||
Owner: s.Owner,
|
||||
Claimer: s.Claimer,
|
||||
ClaimCommitment: s.ClaimCommitment,
|
||||
RefundCommitment: s.RefundCommitment,
|
||||
Timeout1: s.Timeout1,
|
||||
Timeout2: s.Timeout2,
|
||||
Asset: s.Asset,
|
||||
Value: s.Value,
|
||||
Nonce: s.Nonce,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -19,21 +19,21 @@ import (
|
||||
|
||||
func TestSwapCreatorSwap_JSON(t *testing.T) {
|
||||
sf := &SwapCreatorSwap{
|
||||
Owner: ethcommon.HexToAddress("0xda9dfa130df4de4673b89022ee50ff26f6ea73cf"),
|
||||
Claimer: ethcommon.HexToAddress("0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"),
|
||||
PubKeyClaim: ethcommon.HexToHash("0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9"),
|
||||
PubKeyRefund: ethcommon.HexToHash("0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14"),
|
||||
Timeout1: big.NewInt(1672531200),
|
||||
Timeout2: big.NewInt(1672545600),
|
||||
Asset: ethcommon.HexToAddress("0xdac17f958d2ee523a2206206994597c13d831ec7"),
|
||||
Value: coins.EtherToWei(apd.New(9876, 0)).BigInt(),
|
||||
Nonce: big.NewInt(1234),
|
||||
Owner: ethcommon.HexToAddress("0xda9dfa130df4de4673b89022ee50ff26f6ea73cf"),
|
||||
Claimer: ethcommon.HexToAddress("0xbe0eb53f46cd790cd13851d5eff43d12404d33e8"),
|
||||
ClaimCommitment: ethcommon.HexToHash("0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9"),
|
||||
RefundCommitment: ethcommon.HexToHash("0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14"),
|
||||
Timeout1: big.NewInt(1672531200),
|
||||
Timeout2: big.NewInt(1672545600),
|
||||
Asset: ethcommon.HexToAddress("0xdac17f958d2ee523a2206206994597c13d831ec7"),
|
||||
Value: coins.EtherToWei(apd.New(9876, 0)).BigInt(),
|
||||
Nonce: big.NewInt(1234),
|
||||
}
|
||||
expectedJSON := `{
|
||||
"owner": "0xda9dfa130df4de4673b89022ee50ff26f6ea73cf",
|
||||
"claimer": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8",
|
||||
"pubKeyClaim": "0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9",
|
||||
"pubKeyRefund": "0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14",
|
||||
"claimCommitment": "0x5ab9467e70d4e98567991f0179d1f82a3096ed7973f7aff9ea50f649cafa88b9",
|
||||
"refundCommitment": "0x4897bc3b9e02c2a8cd6353b9b29377157bf2694daaf52b59c0b42daa39877f14",
|
||||
"timeout1": 1672531200,
|
||||
"timeout2": 1672545600,
|
||||
"asset": "0xdac17f958d2ee523a2206206994597c13d831ec7",
|
||||
|
||||
@@ -72,10 +72,10 @@ func testNewSwap(t *testing.T, asset types.EthAsset, erc20Contract *TestERC20) {
|
||||
owner := crypto.PubkeyToAddress(pk.PublicKey)
|
||||
claimer := common.EthereumPrivateKeyToAddress(tests.GetMakerTestKey(t))
|
||||
|
||||
var pubKeyClaim, pubKeyRefund [32]byte
|
||||
_, err := rand.Read(pubKeyClaim[:])
|
||||
var claimCommitment, refundCommitment [32]byte
|
||||
_, err := rand.Read(claimCommitment[:])
|
||||
require.NoError(t, err)
|
||||
_, err = rand.Read(pubKeyRefund[:])
|
||||
_, err = rand.Read(refundCommitment[:])
|
||||
require.NoError(t, err)
|
||||
|
||||
nonce, err := rand.Prime(rand.Reader, 256)
|
||||
@@ -91,8 +91,8 @@ func testNewSwap(t *testing.T, asset types.EthAsset, erc20Contract *TestERC20) {
|
||||
|
||||
tx, err := swapCreator.NewSwap(
|
||||
txOpts,
|
||||
pubKeyClaim,
|
||||
pubKeyRefund,
|
||||
claimCommitment,
|
||||
refundCommitment,
|
||||
claimer,
|
||||
defaultTimeoutDuration,
|
||||
defaultTimeoutDuration,
|
||||
@@ -127,15 +127,15 @@ func testNewSwap(t *testing.T, asset types.EthAsset, erc20Contract *TestERC20) {
|
||||
|
||||
// validate that off-chain swapID calculation matches the on-chain value
|
||||
swap := SwapCreatorSwap{
|
||||
Owner: owner,
|
||||
Claimer: claimer,
|
||||
PubKeyClaim: pubKeyClaim,
|
||||
PubKeyRefund: pubKeyRefund,
|
||||
Timeout1: t1,
|
||||
Timeout2: t2,
|
||||
Asset: asset.Address(),
|
||||
Value: value,
|
||||
Nonce: nonce,
|
||||
Owner: owner,
|
||||
Claimer: claimer,
|
||||
ClaimCommitment: claimCommitment,
|
||||
RefundCommitment: refundCommitment,
|
||||
Timeout1: t1,
|
||||
Timeout2: t2,
|
||||
Asset: asset.Address(),
|
||||
Value: value,
|
||||
Nonce: nonce,
|
||||
}
|
||||
|
||||
// validate our off-net calculation of the SwapID
|
||||
@@ -189,15 +189,15 @@ func TestSwapCreator_Claim_vec(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
swap := SwapCreatorSwap{
|
||||
Owner: addr,
|
||||
Claimer: addr,
|
||||
PubKeyClaim: cmt,
|
||||
PubKeyRefund: dummySwapKey,
|
||||
Timeout1: t1,
|
||||
Timeout2: t2,
|
||||
Asset: ethcommon.Address(types.EthAssetETH),
|
||||
Value: defaultSwapValue,
|
||||
Nonce: nonce,
|
||||
Owner: addr,
|
||||
Claimer: addr,
|
||||
ClaimCommitment: cmt,
|
||||
RefundCommitment: dummySwapKey,
|
||||
Timeout1: t1,
|
||||
Timeout2: t2,
|
||||
Asset: ethcommon.Address(types.EthAssetETH),
|
||||
Value: defaultSwapValue,
|
||||
Nonce: nonce,
|
||||
}
|
||||
|
||||
// set contract to Ready
|
||||
@@ -272,15 +272,15 @@ func testClaim(t *testing.T, asset types.EthAsset, newLogIndex int, value *big.I
|
||||
require.NoError(t, err)
|
||||
|
||||
swap := SwapCreatorSwap{
|
||||
Owner: addr,
|
||||
Claimer: addr,
|
||||
PubKeyClaim: cmt,
|
||||
PubKeyRefund: dummySwapKey,
|
||||
Timeout1: t1,
|
||||
Timeout2: t2,
|
||||
Asset: asset.Address(),
|
||||
Value: value,
|
||||
Nonce: nonce,
|
||||
Owner: addr,
|
||||
Claimer: addr,
|
||||
ClaimCommitment: cmt,
|
||||
RefundCommitment: dummySwapKey,
|
||||
Timeout1: t1,
|
||||
Timeout2: t2,
|
||||
Asset: asset.Address(),
|
||||
Value: value,
|
||||
Nonce: nonce,
|
||||
}
|
||||
|
||||
// ensure we can't claim before setting contract to Ready
|
||||
@@ -365,15 +365,15 @@ func testRefundBeforeT1(t *testing.T, asset types.EthAsset, erc20Contract *TestE
|
||||
require.NoError(t, err)
|
||||
|
||||
swap := SwapCreatorSwap{
|
||||
Owner: addr,
|
||||
Claimer: addr,
|
||||
PubKeyClaim: dummySwapKey,
|
||||
PubKeyRefund: cmt,
|
||||
Timeout1: t1,
|
||||
Timeout2: t2,
|
||||
Asset: asset.Address(),
|
||||
Value: defaultSwapValue,
|
||||
Nonce: nonce,
|
||||
Owner: addr,
|
||||
Claimer: addr,
|
||||
ClaimCommitment: dummySwapKey,
|
||||
RefundCommitment: cmt,
|
||||
Timeout1: t1,
|
||||
Timeout2: t2,
|
||||
Asset: asset.Address(),
|
||||
Value: defaultSwapValue,
|
||||
Nonce: nonce,
|
||||
}
|
||||
|
||||
// now let's try to refund
|
||||
@@ -454,15 +454,15 @@ func testRefundAfterT2(t *testing.T, asset types.EthAsset, erc20Contract *TestER
|
||||
// ensure we can't refund between T1 and T2
|
||||
<-time.After(time.Until(time.Unix(t1.Int64()+1, 0)))
|
||||
swap := SwapCreatorSwap{
|
||||
Owner: addr,
|
||||
Claimer: addr,
|
||||
PubKeyClaim: dummySwapKey,
|
||||
PubKeyRefund: cmt,
|
||||
Timeout1: t1,
|
||||
Timeout2: t2,
|
||||
Asset: asset.Address(),
|
||||
Value: defaultSwapValue,
|
||||
Nonce: nonce,
|
||||
Owner: addr,
|
||||
Claimer: addr,
|
||||
ClaimCommitment: dummySwapKey,
|
||||
RefundCommitment: cmt,
|
||||
Timeout1: t1,
|
||||
Timeout2: t2,
|
||||
Asset: asset.Address(),
|
||||
Value: defaultSwapValue,
|
||||
Nonce: nonce,
|
||||
}
|
||||
|
||||
secret := proof.Secret()
|
||||
@@ -533,15 +533,15 @@ func TestSwapCreator_MultipleSwaps(t *testing.T) {
|
||||
addrSwap := crypto.PubkeyToAddress(*sc.walletKey.Public().(*ecdsa.PublicKey))
|
||||
|
||||
sc.swap = SwapCreatorSwap{
|
||||
Owner: addrSwap,
|
||||
Claimer: addrSwap,
|
||||
PubKeyClaim: res.Secp256k1PublicKey().Keccak256(),
|
||||
PubKeyRefund: dummySwapKey, // no one calls refund in this test
|
||||
Timeout1: nil, // timeouts initialised when swap is created
|
||||
Timeout2: nil,
|
||||
Asset: ethcommon.Address(types.EthAssetETH),
|
||||
Value: defaultSwapValue,
|
||||
Nonce: big.NewInt(int64(i)),
|
||||
Owner: addrSwap,
|
||||
Claimer: addrSwap,
|
||||
ClaimCommitment: res.Secp256k1PublicKey().Keccak256(),
|
||||
RefundCommitment: dummySwapKey, // no one calls refund in this test
|
||||
Timeout1: nil, // timeouts initialised when swap is created
|
||||
Timeout2: nil,
|
||||
Asset: ethcommon.Address(types.EthAssetETH),
|
||||
Value: defaultSwapValue,
|
||||
Nonce: big.NewInt(int64(i)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,8 +559,8 @@ func TestSwapCreator_MultipleSwaps(t *testing.T) {
|
||||
auth.Value = sc.swap.Value
|
||||
tx, err := swapCreator.NewSwap(
|
||||
auth,
|
||||
sc.swap.PubKeyClaim,
|
||||
sc.swap.PubKeyRefund,
|
||||
sc.swap.ClaimCommitment,
|
||||
sc.swap.RefundCommitment,
|
||||
sc.swap.Claimer,
|
||||
defaultTimeoutDuration,
|
||||
defaultTimeoutDuration,
|
||||
|
||||
@@ -120,8 +120,8 @@ func (s *SwapCreatorRelaySwap) Hash() types.Hash {
|
||||
args, err := arguments.Pack(
|
||||
s.Swap.Owner,
|
||||
s.Swap.Claimer,
|
||||
s.Swap.PubKeyClaim,
|
||||
s.Swap.PubKeyRefund,
|
||||
s.Swap.ClaimCommitment,
|
||||
s.Swap.RefundCommitment,
|
||||
s.Swap.Timeout1,
|
||||
s.Swap.Timeout2,
|
||||
s.Swap.Asset,
|
||||
@@ -192,8 +192,8 @@ func (sfs *SwapCreatorSwap) SwapID() types.Hash {
|
||||
args, err := arguments.Pack(
|
||||
sfs.Owner,
|
||||
sfs.Claimer,
|
||||
sfs.PubKeyClaim,
|
||||
sfs.PubKeyRefund,
|
||||
sfs.ClaimCommitment,
|
||||
sfs.RefundCommitment,
|
||||
sfs.Timeout1,
|
||||
sfs.Timeout2,
|
||||
sfs.Asset,
|
||||
|
||||
@@ -84,7 +84,7 @@ func (h *Host) receiveInitiateResponse(stream libp2pnetwork.Stream, s SwapState)
|
||||
|
||||
err := s.HandleProtocolMessage(msg)
|
||||
if err != nil {
|
||||
log.Warnf("failed to handle protocol message: err=%s", err)
|
||||
log.Errorf("failed to handle protocol message: %s", err)
|
||||
return
|
||||
}
|
||||
case <-time.After(initiateResponseTimeout):
|
||||
|
||||
@@ -59,15 +59,15 @@ func createTestClaimRequest() *message.RelayClaimRequest {
|
||||
sig := [65]byte{0x1}
|
||||
|
||||
swap := contracts.SwapCreatorSwap{
|
||||
Owner: ethcommon.Address{0x1},
|
||||
Claimer: ethcommon.Address{0x1},
|
||||
PubKeyClaim: [32]byte{0x1},
|
||||
PubKeyRefund: [32]byte{0x1},
|
||||
Timeout1: big.NewInt(time.Now().Add(30 * time.Minute).Unix()),
|
||||
Timeout2: big.NewInt(time.Now().Add(60 * time.Minute).Unix()),
|
||||
Asset: ethcommon.Address(types.EthAssetETH),
|
||||
Value: big.NewInt(1e18),
|
||||
Nonce: big.NewInt(1),
|
||||
Owner: ethcommon.Address{0x1},
|
||||
Claimer: ethcommon.Address{0x1},
|
||||
ClaimCommitment: [32]byte{0x1},
|
||||
RefundCommitment: [32]byte{0x1},
|
||||
Timeout1: big.NewInt(time.Now().Add(30 * time.Minute).Unix()),
|
||||
Timeout2: big.NewInt(time.Now().Add(60 * time.Minute).Unix()),
|
||||
Asset: ethcommon.Address(types.EthAssetETH),
|
||||
Value: big.NewInt(1e18),
|
||||
Nonce: big.NewInt(1),
|
||||
}
|
||||
|
||||
req := &message.RelayClaimRequest{
|
||||
|
||||
@@ -111,22 +111,23 @@ func (s *ExternalSender) approve(
|
||||
|
||||
// NewSwap prompts the external sender to sign a new_swap transaction
|
||||
func (s *ExternalSender) NewSwap(
|
||||
pubKeyClaim [32]byte,
|
||||
pubKeyRefund [32]byte,
|
||||
claimCommitment [32]byte,
|
||||
refundCommitment [32]byte,
|
||||
claimer ethcommon.Address,
|
||||
timeoutDuration *big.Int,
|
||||
nonce *big.Int,
|
||||
amount coins.EthAssetAmount,
|
||||
) (ethcommon.Hash, error) {
|
||||
saveNewSwapTxCallback func(txHash ethcommon.Hash) error,
|
||||
) (*ethtypes.Receipt, error) {
|
||||
// TODO: Add ERC20 token support and approve new_swap for the token transfer
|
||||
if amount.IsToken() {
|
||||
return ethcommon.Hash{}, errors.New("external sender does not support ERC20 token swaps")
|
||||
return nil, errors.New("external sender does not support ERC20 token swaps")
|
||||
}
|
||||
|
||||
input, err := s.abi.Pack("new_swap", pubKeyClaim, pubKeyRefund, claimer, timeoutDuration,
|
||||
input, err := s.abi.Pack("new_swap", claimCommitment, refundCommitment, claimer, timeoutDuration,
|
||||
amount.TokenAddress(), amount.BigInt(), nonce)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tx := &Transaction{
|
||||
@@ -142,11 +143,15 @@ func (s *ExternalSender) NewSwap(
|
||||
var txHash ethcommon.Hash
|
||||
select {
|
||||
case <-time.After(transactionTimeout):
|
||||
return ethcommon.Hash{}, errTransactionTimeout
|
||||
return nil, errTransactionTimeout
|
||||
case txHash = <-s.in:
|
||||
}
|
||||
|
||||
return txHash, nil
|
||||
if err := saveNewSwapTxCallback(txHash); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return block.WaitForReceipt(s.ctx, s.ec, txHash)
|
||||
}
|
||||
|
||||
// SetReady prompts the external sender to sign a set_ready transaction
|
||||
@@ -199,10 +204,5 @@ func (s *ExternalSender) sendAndReceive(input []byte, to ethcommon.Address) (*et
|
||||
case txHash = <-s.in:
|
||||
}
|
||||
|
||||
receipt, err := block.WaitForReceipt(s.ctx, s.ec, txHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return receipt, nil
|
||||
return block.WaitForReceipt(s.ctx, s.ec, txHash)
|
||||
}
|
||||
|
||||
@@ -10,9 +10,12 @@ package txsender
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/cockroachdb/apd/v3"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
logging "github.com/ipfs/go-log"
|
||||
@@ -33,13 +36,14 @@ type Sender interface {
|
||||
SetSwapCreator(*contracts.SwapCreator)
|
||||
SetSwapCreatorAddr(ethcommon.Address)
|
||||
NewSwap(
|
||||
pubKeyClaim [32]byte,
|
||||
pubKeyRefund [32]byte,
|
||||
claimCommitment [32]byte,
|
||||
refundCommitment [32]byte,
|
||||
claimer ethcommon.Address,
|
||||
timeoutDuration *big.Int,
|
||||
nonce *big.Int,
|
||||
amount coins.EthAssetAmount,
|
||||
) (ethcommon.Hash, error)
|
||||
saveNewSwapTxCallback func(txHash ethcommon.Hash) error,
|
||||
) (*ethtypes.Receipt, error)
|
||||
SetReady(swap *contracts.SwapCreatorSwap) (*ethtypes.Receipt, error)
|
||||
Claim(swap *contracts.SwapCreatorSwap, secret [32]byte) (*ethtypes.Receipt, error)
|
||||
Refund(swap *contracts.SwapCreatorSwap, secret [32]byte) (*ethtypes.Receipt, error)
|
||||
@@ -77,45 +81,32 @@ func (s *privateKeySender) SetSwapCreator(contract *contracts.SwapCreator) {
|
||||
func (s *privateKeySender) SetSwapCreatorAddr(_ ethcommon.Address) {}
|
||||
|
||||
func (s *privateKeySender) NewSwap(
|
||||
pubKeyClaim [32]byte,
|
||||
pubKeyRefund [32]byte,
|
||||
claimCommitment [32]byte,
|
||||
refundCommitment [32]byte,
|
||||
claimer ethcommon.Address,
|
||||
timeoutDuration *big.Int,
|
||||
nonce *big.Int,
|
||||
amount coins.EthAssetAmount,
|
||||
) (ethcommon.Hash, error) {
|
||||
// note: the caller must lock the ethclient.
|
||||
saveNewSwapTxCallback func(txHash ethcommon.Hash) error,
|
||||
) (*ethtypes.Receipt, error) {
|
||||
// For token swaps, approving our contract to transfer tokens, and calling
|
||||
// NewSwap which performs the transfer, need to be inside the same wallet
|
||||
// lock grab in case there are other simultaneous swaps happening with the
|
||||
// same token.
|
||||
s.ethClient.Lock()
|
||||
defer s.ethClient.Unlock()
|
||||
|
||||
value := amount.BigInt()
|
||||
|
||||
// For token swaps, approving our contract to transfer tokens, and calling
|
||||
// NewSwap which performs the transfer, needs to be inside the same wallet
|
||||
// lock grab in case there are other simultaneous swaps happening with the
|
||||
// same token.
|
||||
if amount.IsToken() {
|
||||
txOpts, err := s.ethClient.TxOpts(s.ctx)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, err
|
||||
if err := s.approveTransferFrom(amount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tx, err := s.erc20Contract.Approve(txOpts, s.swapCreatorAddr, value)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, fmt.Errorf("approve tx creation failed, %w", err)
|
||||
}
|
||||
|
||||
receipt, err := block.WaitForReceipt(s.ctx, s.ethClient.Raw(), tx.Hash())
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, fmt.Errorf("approve failed, %w", err)
|
||||
}
|
||||
|
||||
log.Debugf("approve transaction included %s", common.ReceiptInfo(receipt))
|
||||
log.Infof("%s %s approved for use by SwapCreator's new_swap",
|
||||
amount.AsStdString(), amount.StdSymbol())
|
||||
}
|
||||
|
||||
txOpts, err := s.ethClient.TxOpts(s.ctx)
|
||||
if err != nil {
|
||||
return ethcommon.Hash{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// transfer ETH if we're not doing an ERC20 swap
|
||||
@@ -123,14 +114,108 @@ func (s *privateKeySender) NewSwap(
|
||||
txOpts.Value = value
|
||||
}
|
||||
|
||||
tx, err := s.swapCreator.NewSwap(txOpts, pubKeyClaim, pubKeyRefund, claimer, timeoutDuration, timeoutDuration,
|
||||
tx, err := s.swapCreator.NewSwap(txOpts, claimCommitment, refundCommitment, claimer, timeoutDuration, timeoutDuration,
|
||||
amount.TokenAddress(), value, nonce)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("new_swap tx creation failed, %w", err)
|
||||
return ethcommon.Hash{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tx.Hash(), nil
|
||||
if err = saveNewSwapTxCallback(tx.Hash()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
receipt, err := block.WaitForReceipt(s.ctx, s.ethClient.Raw(), tx.Hash())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("NewSwap tx %s failed waiting for receipt, %w", tx.Hash(), err)
|
||||
}
|
||||
|
||||
log.Infof("newSwap TX succeeded, %s", common.ReceiptInfo(receipt))
|
||||
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
// approveTransferFrom grants the SwapCreator contract permission to transfer
|
||||
// the passed amount of tokens. Since the transfer happens inside the NewSwap
|
||||
// transaction, we need to grant the contract's address approval for the
|
||||
// transfer amount first. The ethClient lock should already have been grabbed
|
||||
// before invoking this method.
|
||||
func (s *privateKeySender) approveTransferFrom(amount coins.EthAssetAmount) error {
|
||||
if !amount.IsToken() {
|
||||
panic("this function should only be called with a token asset")
|
||||
}
|
||||
|
||||
if amount.AsStd().IsZero() {
|
||||
return errors.New("approveContractToTransfer can not be called with a zero amount")
|
||||
}
|
||||
|
||||
balance, err := s.ethClient.ERC20Balance(s.ctx, amount.TokenAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if balance.AsStd().Cmp(amount.AsStd()) < 0 {
|
||||
return fmt.Errorf("balance of %s %s is under the %s swap amount",
|
||||
balance.AsStdString(), balance.StdSymbol(), amount.AsStdString())
|
||||
}
|
||||
|
||||
token := balance.TokenInfo
|
||||
|
||||
// Make a free call to the Allowance function to determine if we need to spend
|
||||
// gas approving the SwapCreator contract to transfer the token.
|
||||
bindOpts := &bind.CallOpts{Context: s.ctx}
|
||||
allowedAmtBI, err := s.erc20Contract.Allowance(bindOpts, s.ethClient.Address(), s.swapCreatorAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allowedAmt := coins.NewERC20TokenAmountFromBigInt(allowedAmtBI, token)
|
||||
|
||||
if amount.AsStd().Cmp(allowedAmt.AsStd()) <= 0 {
|
||||
log.Infof("swapCreator was already approved to transfer %s %s (needed %s)",
|
||||
allowedAmt.AsStdString(), allowedAmt.StdSymbol(), amount.AsStdString())
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the previous approved amount is not zero, some ERC20 contracts like
|
||||
// USDT require us to first zero out the approved amount before setting
|
||||
// a new value.
|
||||
if !allowedAmt.AsStd().IsZero() {
|
||||
log.Debugf("zeroing approved token amount before raising limit from %s to %s %s",
|
||||
allowedAmt.AsStdString(), amount.AsStdString(), amount.StdSymbol())
|
||||
|
||||
zeroTokenAmt := coins.NewTokenAmountFromDecimals(new(apd.Decimal), token)
|
||||
if err = s.approveNoChecks(zeroTokenAmt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return s.approveNoChecks(amount)
|
||||
}
|
||||
|
||||
// approveNoChecks is a helper method to gives the SwapCreator contract
|
||||
// permission to transfer the passed-in amount of tokens. It's caller should be
|
||||
// doing other checks and have already grabbed the the ethClient's lock.
|
||||
func (s *privateKeySender) approveNoChecks(amount coins.EthAssetAmount) error {
|
||||
txOpts, err := s.ethClient.TxOpts(s.ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := s.erc20Contract.Approve(txOpts, s.swapCreatorAddr, amount.BigInt())
|
||||
if err != nil {
|
||||
return fmt.Errorf("token approve tx for %s %s creation failed, %w",
|
||||
amount.AsStdString(), amount.StdSymbol(), err)
|
||||
}
|
||||
|
||||
receipt, err := block.WaitForReceipt(s.ctx, s.ethClient.Raw(), tx.Hash())
|
||||
if err != nil {
|
||||
return fmt.Errorf("approveNoChecks tx %s failed waiting for receipt, %w", tx.Hash(), err)
|
||||
}
|
||||
|
||||
log.Infof("%s %s approved for use by SwapCreator's new_swap, %s",
|
||||
amount.AsStdString(), amount.StdSymbol(), common.ReceiptInfo(receipt))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *privateKeySender) SetReady(swap *contracts.SwapCreatorSwap) (*ethtypes.Receipt, error) {
|
||||
|
||||
84
protocol/txsender/sender_test.go
Normal file
84
protocol/txsender/sender_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package txsender
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/cockroachdb/apd/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/cliutil"
|
||||
"github.com/athanorlabs/atomic-swap/coins"
|
||||
contracts "github.com/athanorlabs/atomic-swap/ethereum"
|
||||
"github.com/athanorlabs/atomic-swap/ethereum/extethclient"
|
||||
"github.com/athanorlabs/atomic-swap/tests"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cliutil.SetLogLevels("debug")
|
||||
}
|
||||
|
||||
func newTestSender(t *testing.T) (*privateKeySender, *coins.ERC20TokenInfo) {
|
||||
ctx := context.Background()
|
||||
pk := tests.GetTakerTestKey(t)
|
||||
ec := extethclient.CreateTestClient(t, pk)
|
||||
|
||||
swapCreatorAddr, swapCreator := contracts.DevDeploySwapCreator(t, ec.Raw(), pk)
|
||||
|
||||
token := contracts.GetMockTether(t, ec.Raw(), pk)
|
||||
tokenBinding, err := contracts.NewIERC20(token.Address, ec.Raw())
|
||||
require.NoError(t, err)
|
||||
|
||||
sender := NewSenderWithPrivateKey(ctx, ec, swapCreatorAddr, swapCreator, tokenBinding)
|
||||
return sender.(*privateKeySender), token
|
||||
}
|
||||
|
||||
// Verify that our test token behaves like Tether in that changing the approval amount
|
||||
// must be set to zero before it can be changed to a non-zero value.
|
||||
func Test_privateKeySender_approve(t *testing.T) {
|
||||
sender, token := newTestSender(t)
|
||||
|
||||
zeroAmt := coins.NewTokenAmountFromDecimals(new(apd.Decimal), token)
|
||||
tooMuchAmt := coins.NewTokenAmountFromDecimals(coins.StrToDecimal("1000000"), token)
|
||||
|
||||
// Ensure this fails, approving zero amounts should be done using approveNoChecks
|
||||
err := sender.approveTransferFrom(zeroAmt)
|
||||
require.ErrorContains(t, err, "can not be called with a zero amount")
|
||||
|
||||
// Ensure our balance check failure works
|
||||
err = sender.approveTransferFrom(tooMuchAmt)
|
||||
require.ErrorContains(t, err, " is under ")
|
||||
|
||||
// Make sure that we always start testing in a know state, where the swapCreator
|
||||
// contract is not approved to transfer any token amount
|
||||
err = sender.approveNoChecks(zeroAmt)
|
||||
require.NoError(t, err)
|
||||
|
||||
// First approve succeeds, as the current allowance is zero
|
||||
amt := coins.NewTokenAmountFromDecimals(coins.StrToDecimal("3"), token)
|
||||
err = sender.approveTransferFrom(amt)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Second approve succeeds, as we are already approved for more than is being
|
||||
// asked for. This is an attempt to optimize gas, but it should happen very
|
||||
// often. The only way you can easily get into this state is if a previous
|
||||
// swap failed after approve but before a successful NewSwap transaction.
|
||||
amt = coins.NewTokenAmountFromDecimals(coins.StrToDecimal("2"), token)
|
||||
err = sender.approveTransferFrom(amt)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Third approve fails, because we are trying to increase the approval
|
||||
// amount without zeroing it first. Contracts like Tether do not allow this,
|
||||
// as it gives the contract being granted approval the potential to quickly
|
||||
// transfer the previously approved amount before the new approval gets
|
||||
// mined and then do a second transfer using the new approved amount.
|
||||
amt = coins.NewTokenAmountFromDecimals(coins.StrToDecimal("4"), token)
|
||||
err = sender.approveNoChecks(amt)
|
||||
require.ErrorContains(t, err, `token approve tx for 4 "USDT" creation failed`)
|
||||
|
||||
// The next approval, of the same amount that just failed, is actually 2
|
||||
// approvals. When calling approveTransferFrom, the code will see that the
|
||||
// approval amount needs to be raised and zero it out before raising it.
|
||||
err = sender.approveTransferFrom(amt)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -101,15 +101,15 @@ func newTestSwap(
|
||||
require.NoError(t, err)
|
||||
|
||||
contractSwap := &contracts.SwapCreatorSwap{
|
||||
Owner: ethAddr,
|
||||
Claimer: ethAddr,
|
||||
PubKeyClaim: claimKey,
|
||||
PubKeyRefund: refundKey,
|
||||
Timeout1: t1,
|
||||
Timeout2: t2,
|
||||
Asset: ethcommon.Address(asset),
|
||||
Value: amount,
|
||||
Nonce: nonce,
|
||||
Owner: ethAddr,
|
||||
Claimer: ethAddr,
|
||||
ClaimCommitment: claimKey,
|
||||
RefundCommitment: refundKey,
|
||||
Timeout1: t1,
|
||||
Timeout2: t2,
|
||||
Asset: ethcommon.Address(asset),
|
||||
Value: amount,
|
||||
Nonce: nonce,
|
||||
}
|
||||
|
||||
return contractSwap, contractSwapID, tx.Hash()
|
||||
|
||||
@@ -195,15 +195,15 @@ func (inst *Instance) refundOrCancelNewSwap(s *swap.Info, txHash ethcommon.Hash)
|
||||
}
|
||||
|
||||
swap := contracts.SwapCreatorSwap{
|
||||
Owner: params.owner,
|
||||
Claimer: params.claimer,
|
||||
PubKeyClaim: params.cmtXMRMaker,
|
||||
PubKeyRefund: params.cmtXMRTaker,
|
||||
Timeout1: t1,
|
||||
Timeout2: t2,
|
||||
Asset: params.asset,
|
||||
Value: params.value,
|
||||
Nonce: params.nonce,
|
||||
Owner: params.owner,
|
||||
Claimer: params.claimer,
|
||||
ClaimCommitment: params.cmtXMRMaker,
|
||||
RefundCommitment: params.cmtXMRTaker,
|
||||
Timeout1: t1,
|
||||
Timeout2: t2,
|
||||
Asset: params.asset,
|
||||
Value: params.value,
|
||||
Nonce: params.nonce,
|
||||
}
|
||||
|
||||
// our secret value
|
||||
@@ -464,8 +464,8 @@ func getNewSwapParametersFromTx(
|
||||
}
|
||||
|
||||
claimer := newSwapInputs["_claimer"].(ethcommon.Address)
|
||||
cmtXMRMaker := newSwapInputs["_pubKeyClaim"].([32]byte)
|
||||
cmtXMRTaker := newSwapInputs["_pubKeyRefund"].([32]byte)
|
||||
cmtXMRMaker := newSwapInputs["_claimCommitment"].([32]byte)
|
||||
cmtXMRTaker := newSwapInputs["_refundCommitment"].([32]byte)
|
||||
asset := newSwapInputs["_asset"].(ethcommon.Address)
|
||||
value := newSwapInputs["_value"].(*big.Int)
|
||||
nonce := newSwapInputs["_nonce"].(*big.Int)
|
||||
|
||||
@@ -107,7 +107,7 @@ func Test_validateMinBalForTokenSwap_InsufficientETHBalance(t *testing.T) {
|
||||
// Amount in check below is truncated, so minor adjustments in the
|
||||
// expected gas won't break the test. The full message looks like:
|
||||
// "balance of 0.007 ETH is under required amount of 0.00743302 ETH"
|
||||
require.ErrorContains(t, err, `balance of 0.007 ETH is under required amount of 0.0074`)
|
||||
require.ErrorContains(t, err, `balance of 0.007 ETH is under required amount of 0.0075`)
|
||||
}
|
||||
|
||||
func Test_validateMinBalanceETH(t *testing.T) {
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"github.com/athanorlabs/atomic-swap/db"
|
||||
"github.com/athanorlabs/atomic-swap/dleq"
|
||||
contracts "github.com/athanorlabs/atomic-swap/ethereum"
|
||||
"github.com/athanorlabs/atomic-swap/ethereum/block"
|
||||
"github.com/athanorlabs/atomic-swap/ethereum/watcher"
|
||||
"github.com/athanorlabs/atomic-swap/monero"
|
||||
"github.com/athanorlabs/atomic-swap/net/message"
|
||||
@@ -607,7 +606,7 @@ func (s *swapState) lockAsset() (*ethtypes.Receipt, error) {
|
||||
nonce := contracts.GenerateNewSwapNonce()
|
||||
receipt, err := s.lockAndWaitForReceipt(cmtXMRMaker, cmtXMRTaker, nonce)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to lock asset: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("instantiated swap on-chain: amount=%s asset=%s %s",
|
||||
@@ -643,15 +642,15 @@ func (s *swapState) lockAsset() (*ethtypes.Receipt, error) {
|
||||
s.setTimeouts(t1, t2)
|
||||
|
||||
s.contractSwap = &contracts.SwapCreatorSwap{
|
||||
Owner: s.ETHClient().Address(),
|
||||
Claimer: s.xmrmakerAddress,
|
||||
PubKeyClaim: cmtXMRMaker,
|
||||
PubKeyRefund: cmtXMRTaker,
|
||||
Timeout1: t1,
|
||||
Timeout2: t2,
|
||||
Asset: ethcommon.Address(s.info.EthAsset),
|
||||
Value: s.providedAmount.BigInt(),
|
||||
Nonce: nonce,
|
||||
Owner: s.ETHClient().Address(),
|
||||
Claimer: s.xmrmakerAddress,
|
||||
ClaimCommitment: cmtXMRMaker,
|
||||
RefundCommitment: cmtXMRTaker,
|
||||
Timeout1: t1,
|
||||
Timeout2: t2,
|
||||
Asset: ethcommon.Address(s.info.EthAsset),
|
||||
Value: s.providedAmount.BigInt(),
|
||||
Nonce: nonce,
|
||||
}
|
||||
|
||||
ethInfo := &db.EthereumSwapInfo{
|
||||
@@ -673,31 +672,33 @@ func (s *swapState) lockAndWaitForReceipt(
|
||||
cmtXMRMaker, cmtXMRTaker [32]byte,
|
||||
nonce *big.Int,
|
||||
) (*ethtypes.Receipt, error) {
|
||||
s.Backend.ETHClient().Lock()
|
||||
defer s.Backend.ETHClient().Unlock()
|
||||
|
||||
txHash, err := s.sender.NewSwap(
|
||||
// We need a callback to save the TX hash, as we've seen NewSwap
|
||||
// transactions stay in the mempool for hours before getting included in a
|
||||
// block. We want to ensure that the NewSwap TX hash gets stored to the
|
||||
// database even if the NewSwap function call returns an error, because it
|
||||
// failed to get a TX receipt.
|
||||
saveNewSwapTxCallback := func(txHash ethcommon.Hash) error {
|
||||
err := s.Backend.RecoveryDB().PutNewSwapTxHash(s.OfferID(), txHash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write newSwap tx hash to db: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
receipt, err := s.sender.NewSwap(
|
||||
cmtXMRMaker,
|
||||
cmtXMRTaker,
|
||||
s.xmrmakerAddress,
|
||||
big.NewInt(int64(s.SwapTimeout().Seconds())),
|
||||
nonce,
|
||||
s.providedAmount,
|
||||
saveNewSwapTxCallback,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to instantiate swap on-chain: %w", err)
|
||||
}
|
||||
|
||||
err = s.Backend.RecoveryDB().PutNewSwapTxHash(s.OfferID(), txHash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to write newSwap tx hash to db: %w", err)
|
||||
}
|
||||
|
||||
receipt, err := block.WaitForReceipt(s.ctx, s.ETHClient().Raw(), txHash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get newSwap transaction receipt: %w", err)
|
||||
}
|
||||
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -20,15 +20,15 @@ import (
|
||||
|
||||
func createTestSwap(claimer ethcommon.Address) *contracts.SwapCreatorSwap {
|
||||
return &contracts.SwapCreatorSwap{
|
||||
Owner: ethcommon.Address{0x1},
|
||||
Claimer: claimer,
|
||||
PubKeyClaim: [32]byte{0x1},
|
||||
PubKeyRefund: [32]byte{0x1},
|
||||
Timeout1: big.NewInt(time.Now().Add(30 * time.Minute).Unix()),
|
||||
Timeout2: big.NewInt(time.Now().Add(60 * time.Minute).Unix()),
|
||||
Asset: ethcommon.Address(types.EthAssetETH),
|
||||
Value: big.NewInt(1e18),
|
||||
Nonce: big.NewInt(1),
|
||||
Owner: ethcommon.Address{0x1},
|
||||
Claimer: claimer,
|
||||
ClaimCommitment: [32]byte{0x1},
|
||||
RefundCommitment: [32]byte{0x1},
|
||||
Timeout1: big.NewInt(time.Now().Add(30 * time.Minute).Unix()),
|
||||
Timeout2: big.NewInt(time.Now().Add(60 * time.Minute).Unix()),
|
||||
Asset: ethcommon.Address(types.EthAssetETH),
|
||||
Value: big.NewInt(1e18),
|
||||
Nonce: big.NewInt(1),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,15 +88,15 @@ func Test_ValidateAndSendTransaction(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
swap := contracts.SwapCreatorSwap{
|
||||
Owner: relayerAddr,
|
||||
Claimer: claimerAddr,
|
||||
PubKeyClaim: cmt,
|
||||
PubKeyRefund: refundKey,
|
||||
Timeout1: t1,
|
||||
Timeout2: t2,
|
||||
Asset: types.EthAssetETH.Address(),
|
||||
Value: value,
|
||||
Nonce: nonce,
|
||||
Owner: relayerAddr,
|
||||
Claimer: claimerAddr,
|
||||
ClaimCommitment: cmt,
|
||||
RefundCommitment: refundKey,
|
||||
Timeout1: t1,
|
||||
Timeout2: t2,
|
||||
Asset: types.EthAssetETH.Address(),
|
||||
Value: value,
|
||||
Nonce: nonce,
|
||||
}
|
||||
|
||||
// set contract to Ready
|
||||
|
||||
@@ -56,15 +56,15 @@ func TestValidateRelayerFee(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
swap := contracts.SwapCreatorSwap{
|
||||
Owner: ethcommon.Address{},
|
||||
Claimer: ethcommon.Address{},
|
||||
PubKeyClaim: [32]byte{},
|
||||
PubKeyRefund: [32]byte{},
|
||||
Timeout1: new(big.Int),
|
||||
Timeout2: new(big.Int),
|
||||
Asset: ethcommon.Address{},
|
||||
Value: tc.value,
|
||||
Nonce: new(big.Int),
|
||||
Owner: ethcommon.Address{},
|
||||
Claimer: ethcommon.Address{},
|
||||
ClaimCommitment: [32]byte{},
|
||||
RefundCommitment: [32]byte{},
|
||||
Timeout1: new(big.Int),
|
||||
Timeout2: new(big.Int),
|
||||
Asset: ethcommon.Address{},
|
||||
Value: tc.value,
|
||||
Nonce: new(big.Int),
|
||||
}
|
||||
|
||||
request := &message.RelayClaimRequest{
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# Use the project root (one directory above this script) as the current working directory:
|
||||
PROJECT_ROOT="$(dirname "$(dirname "$(realpath "$0")")")"
|
||||
cd "${PROJECT_ROOT}" || exit 1
|
||||
cd "${PROJECT_ROOT}"
|
||||
|
||||
ABIGEN="$(go env GOPATH)/bin/abigen"
|
||||
|
||||
@@ -11,14 +13,27 @@ if [[ -z "${SOLC_BIN}" ]]; then
|
||||
fi
|
||||
|
||||
compile-contract() {
|
||||
local solidity_type_name="${1:?}"
|
||||
local solidity_file_name="${1:?}"
|
||||
local go_type_name="${2:?}"
|
||||
local go_file_name="${3:?}"
|
||||
|
||||
# strip leading path and extension from to get the solidity type name
|
||||
local solidity_type_name
|
||||
solidity_type_name="$(basename "${solidity_file_name%.sol}")"
|
||||
|
||||
echo "Generating go bindings for ${solidity_type_name}"
|
||||
|
||||
"${SOLC_BIN}" --optimize --optimize-runs=200 --abi "ethereum/contracts/${solidity_type_name}.sol" -o ethereum/abi/ --overwrite
|
||||
"${SOLC_BIN}" --optimize --optimize-runs=200 --bin "ethereum/contracts/${solidity_type_name}.sol" -o ethereum/bin/ --overwrite
|
||||
"${SOLC_BIN}" --optimize --optimize-runs=200 \
|
||||
--metadata --metadata-literal \
|
||||
--base-path "ethereum/contracts" \
|
||||
--abi "ethereum/contracts/${solidity_file_name}" \
|
||||
-o ethereum/abi/ --overwrite
|
||||
"${SOLC_BIN}" --optimize --optimize-runs=200 \
|
||||
--base-path ethereum/contracts \
|
||||
--include-path . \
|
||||
--bin "ethereum/contracts/${solidity_file_name}" \
|
||||
-o ethereum/bin/ --overwrite
|
||||
|
||||
"${ABIGEN}" \
|
||||
--abi "ethereum/abi/${solidity_type_name}.abi" \
|
||||
--bin "ethereum/bin/${solidity_type_name}.bin" \
|
||||
@@ -27,7 +42,16 @@ compile-contract() {
|
||||
--out "ethereum/${go_file_name}.go"
|
||||
}
|
||||
|
||||
compile-contract SwapCreator SwapCreator swap_creator
|
||||
compile-contract TestERC20 TestERC20 erc20_token
|
||||
compile-contract IERC20Metadata IERC20 ierc20
|
||||
compile-contract AggregatorV3Interface AggregatorV3Interface aggregator_v3_interface
|
||||
compile-contract SwapCreator.sol SwapCreator swap_creator
|
||||
compile-contract TestERC20.sol TestERC20 erc20_token
|
||||
compile-contract @openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol IERC20 ierc20
|
||||
compile-contract AggregatorV3Interface.sol AggregatorV3Interface aggregator_v3_interface
|
||||
|
||||
# etherscan lets you upload solidity sources in "standard JSON input" format.
|
||||
# With solc 0.8.20+, using the "--metadata --metadata-literal" flags, we can
|
||||
# strip out a bunch of fields from the output metadata to get an input file that
|
||||
# etherscan accepts. etherscan should accept the ".settings.compilationTarget"
|
||||
# field, but the field was only introduced in solc 0.8.20. Try adding it back
|
||||
# later.
|
||||
jq 'del( .output, .compiler, .settings.compilationTarget, .version, .sources[] .license )' \
|
||||
ethereum/abi/SwapCreator_meta.json >ethereum/abi/SwapCreator_etherscan.json
|
||||
|
||||
@@ -47,6 +47,7 @@ var testPackages = []struct {
|
||||
{"protocol/backend", 2},
|
||||
{"protocol/xmrmaker", 3},
|
||||
{"protocol/xmrtaker", 3},
|
||||
{"protocol/txsender", 2},
|
||||
{"recover", 2},
|
||||
{"relayer", 2},
|
||||
{"tests", 3},
|
||||
@@ -71,10 +72,10 @@ var ganacheTestKeys = []string{
|
||||
/* RESERVED KEYS:
|
||||
* "4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d", // ganache key #0
|
||||
* "6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1", // ganache key #1
|
||||
* "6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c", // ganache key #2
|
||||
* "646f1ce2fdad0e6deeeb5c7e8e5543bdde65e86029e2fd9fc169899c440a7913", // ganache key #3
|
||||
* "add53f9a7e588d003326d1cbf9e4a43c061aadd9bc938c843a79e7b4fd2ad743", // ganache key #4
|
||||
*/
|
||||
"6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c", // ganache key #2
|
||||
"646f1ce2fdad0e6deeeb5c7e8e5543bdde65e86029e2fd9fc169899c440a7913", // ganache key #3
|
||||
"add53f9a7e588d003326d1cbf9e4a43c061aadd9bc938c843a79e7b4fd2ad743", // ganache key #4
|
||||
"395df67f0c2d2d9fe1ad08d1bc8b6627011959b79c53d7dd6a3536a33ab8a4fd", // ganache key #5
|
||||
"e485d098507f54e7733a205420dfddbe58db035fa577fc294ebd14db90767a52", // ganache key #6
|
||||
"a453611d9419d0e56f499079478fd72c37b251a94bfde4d19872c44cf65386e3", // ganache key #7
|
||||
|
||||
Reference in New Issue
Block a user