Skip to content

Commit

Permalink
eth/client: Swap
Browse files Browse the repository at this point in the history
  • Loading branch information
martonp committed Nov 3, 2021
1 parent 412baf7 commit ee847d0
Show file tree
Hide file tree
Showing 4 changed files with 703 additions and 59 deletions.
246 changes: 208 additions & 38 deletions client/asset/eth/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const (
defaultGasFeeLimit = 200 // gwei

RedeemGas = 63000 // gas

GasTipCap = 10 // gwei
)

var (
Expand Down Expand Up @@ -187,6 +189,8 @@ type ExchangeWallet struct {
log dex.Logger
tipChange func(error)

networkID int64

internalNode *node.Node

tipMtx sync.RWMutex
Expand Down Expand Up @@ -275,12 +279,18 @@ func NewWallet(assetCFG *asset.WalletConfig, logger dex.Logger, network dex.Netw
len(accounts))
}

networkID, err := getNetworkID(network)
if err != nil {
return nil, fmt.Errorf("NewWallet: unable to get netowrk ID: %w", err)
}

return &ExchangeWallet{
log: logger,
tipChange: assetCFG.TipChange,
internalNode: node,
lockedFunds: make(map[string]uint64),
acct: &accounts[0],
networkID: networkID,
initGasCache: make(map[int]uint64),
}, nil
}
Expand Down Expand Up @@ -508,7 +518,11 @@ func (*ExchangeWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, er

// coin implements the asset.Coin interface for ETH
type coin struct {
id srveth.AmountCoinID
id srveth.CoinID
// the value can be determined from the coin id, but for some
// coin ids a lookup would be required from the blockchain to
// determine its value, so this field is used as a cache.
value uint64
}

// ID is the ETH coins ID. It includes the address the coins came from (20 bytes)
Expand All @@ -525,14 +539,14 @@ func (c *coin) String() string {

// Value returns the value in gwei of the coin.
func (c *coin) Value() uint64 {
return c.id.Amount
return c.value
}

var _ asset.Coin = (*coin)(nil)

// decodeCoinID decodes a coin id into a coin object. The coin id
// decodeAmountCoinID decodes a coin id into a coin object. The coin id
// must contain an AmountCoinID.
func decodeCoinID(coinID []byte) (*coin, error) {
func decodeAmountCoinID(coinID []byte) (*coin, error) {
id, err := srveth.DecodeCoinID(coinID)
if err != nil {
return nil, err
Expand All @@ -545,10 +559,19 @@ func decodeCoinID(coinID []byte) (*coin, error) {
}

return &coin{
id: *amountCoinID,
id: amountCoinID,
value: amountCoinID.Amount,
}, nil
}

func (eth *ExchangeWallet) createAmountCoin(amount uint64) *coin {
id := srveth.CreateAmountCoinID(eth.acct.Address, amount)
return &coin{
id: id,
value: amount,
}
}

// FundOrder selects coins for use in an order. The coins will be locked, and
// will not be returned in subsequent calls to FundOrder or calculated in calls
// to Available, unless they are unlocked with ReturnCoins.
Expand All @@ -558,21 +581,10 @@ func decodeCoinID(coinID []byte) (*coin, error) {
func (eth *ExchangeWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, error) {
maxFees := ord.DEXConfig.MaxFeeRate * ord.DEXConfig.SwapSize * ord.MaxSwapCount
fundsNeeded := ord.Value + maxFees

var nonce [8]byte
copy(nonce[:], encode.RandomBytes(8))
var address [20]byte
copy(address[:], eth.acct.Address.Bytes())
coin := coin{
id: srveth.AmountCoinID{
Address: address,
Amount: fundsNeeded,
Nonce: nonce,
},
}
coins := asset.Coins{&coin}

coins := asset.Coins{eth.createAmountCoin(fundsNeeded)}
eth.lockedFundsMtx.Lock()
err := eth.lockFunds(coins)
eth.lockedFundsMtx.Unlock()
if err != nil {
return nil, nil, err
}
Expand All @@ -583,6 +595,8 @@ func (eth *ExchangeWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes
// ReturnCoins unlocks coins. This would be necessary in the case of a
// canceled order.
func (eth *ExchangeWallet) ReturnCoins(unspents asset.Coins) error {
eth.lockedFundsMtx.Lock()
defer eth.lockedFundsMtx.Unlock()
return eth.unlockFunds(unspents)
}

Expand All @@ -591,19 +605,28 @@ func (eth *ExchangeWallet) ReturnCoins(unspents asset.Coins) error {
func (eth *ExchangeWallet) FundingCoins(ids []dex.Bytes) (asset.Coins, error) {
coins := make([]asset.Coin, 0, len(ids))
for _, id := range ids {
coin, err := decodeCoinID(id)
coin, err := decodeAmountCoinID(id)
if err != nil {
return nil, err
}
if !bytes.Equal(coin.id.Address.Bytes(), eth.acct.Address.Bytes()) {

amountCoinID, ok := coin.id.(*srveth.AmountCoinID)
if !ok {
// this should never happen if decodeAmountCoinID is implemented properly
return nil, errors.New("FundingCoins: coin id must be amount coin id")
}

if !bytes.Equal(amountCoinID.Address.Bytes(), eth.acct.Address.Bytes()) {
return nil, fmt.Errorf("FundingCoins: coin address %v != wallet address %v",
coin.id.Address, eth.acct.Address)
amountCoinID.Address, eth.acct.Address)
}

coins = append(coins, coin)
}

eth.lockedFundsMtx.Lock()
err := eth.lockFunds(coins)
eth.lockedFundsMtx.Unlock()
if err != nil {
return nil, err
}
Expand All @@ -612,10 +635,9 @@ func (eth *ExchangeWallet) FundingCoins(ids []dex.Bytes) (asset.Coins, error) {
}

// lockFunds adds coins to the map of locked funds.
//
// lockedFundsMtx MUST be held when calling this function.
func (eth *ExchangeWallet) lockFunds(coins asset.Coins) error {
eth.lockedFundsMtx.Lock()
defer eth.lockedFundsMtx.Unlock()

currentlyLocking := make(map[string]bool)
var amountToLock uint64
for _, coin := range coins {
Expand Down Expand Up @@ -646,11 +668,10 @@ func (eth *ExchangeWallet) lockFunds(coins asset.Coins) error {
return nil
}

// lockFunds removes coins from the map of locked funds.
// unlockFunds removes coins from the map of locked funds.
//
// lockedFundsMtx MUST be held when calling this function.
func (eth *ExchangeWallet) unlockFunds(coins asset.Coins) error {
eth.lockedFundsMtx.Lock()
defer eth.lockedFundsMtx.Unlock()

currentlyUnlocking := make(map[string]bool)
for _, coin := range coins {
hexID := hex.EncodeToString(coin.ID())
Expand All @@ -670,12 +691,155 @@ func (eth *ExchangeWallet) unlockFunds(coins asset.Coins) error {
return nil
}

// Swap sends the swaps in a single transaction. The Receipts returned can be
// used to refund a failed transaction. The Input coins are manually unlocked
// because they're not auto-unlocked by the wallet and therefore inaccurately
// included as part of the locked balance despite being spent.
func (*ExchangeWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint64, error) {
return nil, nil, 0, asset.ErrNotImplemented
// swapReceipt implements the asset.Receipt interface for ETH.
type swapReceipt struct {
txCoinID srveth.TxCoinID
swapCoinID srveth.SwapCoinID
// expiration and value can be determined using the coin ids with
// a blockchain lookup, but we cache these values to avoid this.
expiration time.Time
value uint64
}

// Expiration returns the time after which the contract can be
// refunded.
func (r *swapReceipt) Expiration() time.Time {
return r.expiration
}

// Coin returns the coin used to fund the swap.
func (r *swapReceipt) Coin() asset.Coin {
return &coin{
value: r.value,
id: &r.txCoinID,
}
}

// Contract returns an encoded srveth.SwapCoinID that can be used to identify a
// swap contract.
func (r *swapReceipt) Contract() dex.Bytes {
return dex.Bytes(r.swapCoinID.Encode())
}

// String returns a string representation of the swapReceipt.
func (r *swapReceipt) String() string {
return fmt.Sprintf("Contract address: %v, Secret hash: %x",
r.swapCoinID.ContractAddress, r.swapCoinID.SecretHash)
}

// SignedRefund returns an empty byte array. ETH does not support a pre-signed
// redeem script becuase the nonce needed in the transaction can not be previously
// determined. We will need to come up with a different way to support manual
// refunds in ETH.
func (*swapReceipt) SignedRefund() dex.Bytes {
return dex.Bytes{}
}

var _ asset.Receipt = (*swapReceipt)(nil)

// Swap sends the swaps in a single transaction. The Receipts returned can
// be used to refund a failed transaction. The fees used returned are the max
// fees that will possibly be used, since in ethereum with EIP-1559 we cannot
// know exactly how much fees will be used.
func (eth *ExchangeWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint64, error) {
initiations := make([]dexeth.ETHSwapInitiation, 0, len(swaps.Contracts))
receipts := make([]asset.Receipt, 0, len(swaps.Contracts))

eth.lockedFundsMtx.Lock()
defer eth.lockedFundsMtx.Unlock()

var totalInputValue uint64
for _, input := range swaps.Inputs {
if _, ok := eth.lockedFunds[input.ID().String()]; !ok {
return nil, nil, 0,
fmt.Errorf("Swap: attempting to use coin that was not locked")
}
totalInputValue += input.Value()
}

var totalContractValue uint64
secretHashMap := make(map[string]bool)
for _, contract := range swaps.Contracts {
totalContractValue += contract.Value

if len(contract.SecretHash) != 32 {
return nil, nil, 0,
fmt.Errorf("Swap: expected secret hash of length 32, but got %v", len(contract.SecretHash))
}
encSecretHash := hex.EncodeToString(contract.SecretHash)
if _, ok := secretHashMap[encSecretHash]; ok {
return nil, nil, 0, fmt.Errorf("Swap: secret hashes must be unique")
}
secretHashMap[encSecretHash] = true
var secretHash [32]byte
copy(secretHash[:], contract.SecretHash)

if !common.IsHexAddress(contract.Address) {
return nil, nil, 0, fmt.Errorf("Swap: address in contract is not valid %v", contract.Address)
}
address := common.HexToAddress(contract.Address)

initiations = append(initiations,
dexeth.ETHSwapInitiation{
RefundTimestamp: big.NewInt(0).SetUint64(contract.LockTime),
SecretHash: secretHash,
Participant: address,
Value: big.NewInt(0).SetUint64(contract.Value),
})
}

initGas, err := eth.getInitGas(len(initiations))
if err != nil {
return nil, nil, 0, err
}

totalUsedValue := totalContractValue + (initGas * swaps.FeeRate)
if totalInputValue < totalUsedValue {
return nil, nil, 0, fmt.Errorf("Swap: coin inputs value %d < required %d", totalInputValue, totalUsedValue)
}

opts := bind.TransactOpts{
From: eth.acct.Address,
Value: big.NewInt(0).SetUint64(totalContractValue),
GasFeeCap: big.NewInt(0).SetUint64(swaps.FeeRate),
GasTipCap: big.NewInt(GasTipCap),
Context: eth.ctx}

tx, err := eth.node.initiate(&opts, eth.networkID, initiations)
if err != nil {
return nil, nil, 0, fmt.Errorf("Swap: initiate error: %w", err)
}

var txID [32]byte
copy(txID[:], tx.Hash().Bytes())
for i, initiation := range initiations {
txCoinID := &srveth.TxCoinID{
TxID: txID,
Index: uint32(i)}
swapCoinID := srveth.SwapCoinID{
ContractAddress: mainnetContractAddr,
SecretHash: initiation.SecretHash}
receipts = append(receipts,
&swapReceipt{
expiration: time.Unix(initiation.RefundTimestamp.Int64(), 0),
value: initiation.Value.Uint64(),
txCoinID: *txCoinID,
swapCoinID: swapCoinID,
})
}

eth.unlockFunds(swaps.Inputs)
var change asset.Coin
changeAmount := totalInputValue - totalUsedValue
if changeAmount > 0 {
change = eth.createAmountCoin(changeAmount)
}
if swaps.LockChange && change != nil {
eth.lockFunds(asset.Coins{change})
}

feesUsed := swaps.FeeRate * initGas
return receipts, change, feesUsed, nil
}

// Redeem sends the redemption transaction, which may contain more than one
Expand All @@ -688,14 +852,20 @@ func (*ExchangeWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin,
// specified funding Coin. Only a coin that came from the address this wallet
// is initialized with can be used to sign.
func (e *ExchangeWallet) SignMessage(coin asset.Coin, msg dex.Bytes) (pubkeys, sigs []dex.Bytes, err error) {
ethCoin, err := decodeCoinID(coin.ID())
ethCoin, err := decodeAmountCoinID(coin.ID())
if err != nil {
return nil, nil, err
}

if !bytes.Equal(ethCoin.id.Address.Bytes(), e.acct.Address.Bytes()) {
amountCoinID, ok := ethCoin.id.(*srveth.AmountCoinID)
if !ok {
// this should never happen if decodeAmountCoinID is implemented properly
return nil, nil, errors.New("FundingCoins: coin id must be amount coin id")
}

if !bytes.Equal(amountCoinID.Address.Bytes(), e.acct.Address.Bytes()) {
return nil, nil, fmt.Errorf("SignMessage: coin address: %v != wallet address: %v",
ethCoin.id.Address, e.acct.Address)
amountCoinID.Address, e.acct.Address)
}

sig, err := e.node.signData(e.acct.Address, msg)
Expand Down
Loading

0 comments on commit ee847d0

Please sign in to comment.