github.com/ethersphere/bee/v2@v2.2.0/pkg/settlement/swap/chequebook/chequebook.go (about)

     1  // Copyright 2020 The Swarm Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package chequebook
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"math/big"
    12  	"strings"
    13  	"sync"
    14  
    15  	"github.com/ethereum/go-ethereum/common"
    16  	"github.com/ethersphere/bee/v2/pkg/sctx"
    17  	"github.com/ethersphere/bee/v2/pkg/settlement/swap/erc20"
    18  	"github.com/ethersphere/bee/v2/pkg/storage"
    19  	"github.com/ethersphere/bee/v2/pkg/transaction"
    20  	"github.com/ethersphere/bee/v2/pkg/util/abiutil"
    21  	"github.com/ethersphere/go-sw3-abi/sw3abi"
    22  )
    23  
    24  // loggerName is the tree path name of the logger for this package.
    25  const loggerName = "chequebook"
    26  
    27  // SendChequeFunc is a function to send cheques.
    28  type SendChequeFunc func(cheque *SignedCheque) error
    29  
    30  const (
    31  	lastIssuedChequeKeyPrefix = "swap_chequebook_last_issued_cheque_"
    32  	totalIssuedKey            = "swap_chequebook_total_issued_"
    33  )
    34  
    35  var (
    36  	// ErrOutOfFunds is the error when the chequebook has not enough free funds for a cheque
    37  	ErrOutOfFunds = errors.New("chequebook out of funds")
    38  	// ErrInsufficientFunds is the error when the chequebook has not enough free funds for a user action
    39  	ErrInsufficientFunds = errors.New("insufficient token balance")
    40  
    41  	chequebookABI          = abiutil.MustParseABI(sw3abi.ERC20SimpleSwapABIv0_6_5)
    42  	chequeCashedEventType  = chequebookABI.Events["ChequeCashed"]
    43  	chequeBouncedEventType = chequebookABI.Events["ChequeBounced"]
    44  )
    45  
    46  // Service is the main interface for interacting with the nodes chequebook.
    47  type Service interface {
    48  	// Deposit starts depositing erc20 token into the chequebook. This returns once the transactions has been broadcast.
    49  	Deposit(ctx context.Context, amount *big.Int) (hash common.Hash, err error)
    50  	// Withdraw starts withdrawing erc20 token from the chequebook. This returns once the transactions has been broadcast.
    51  	Withdraw(ctx context.Context, amount *big.Int) (hash common.Hash, err error)
    52  	// WaitForDeposit waits for the deposit transaction to confirm and verifies the result.
    53  	WaitForDeposit(ctx context.Context, txHash common.Hash) error
    54  	// Balance returns the token balance of the chequebook.
    55  	Balance(ctx context.Context) (*big.Int, error)
    56  	// AvailableBalance returns the token balance of the chequebook which is not yet used for uncashed cheques.
    57  	AvailableBalance(ctx context.Context) (*big.Int, error)
    58  	// Address returns the address of the used chequebook contract.
    59  	Address() common.Address
    60  	// Issue a new cheque for the beneficiary with an cumulativePayout amount higher than the last.
    61  	Issue(ctx context.Context, beneficiary common.Address, amount *big.Int, sendChequeFunc SendChequeFunc) (*big.Int, error)
    62  	// LastCheque returns the last cheque we issued for the beneficiary.
    63  	LastCheque(beneficiary common.Address) (*SignedCheque, error)
    64  	// LastCheque returns the last cheques for all beneficiaries.
    65  	LastCheques() (map[common.Address]*SignedCheque, error)
    66  }
    67  
    68  type service struct {
    69  	lock               sync.Mutex
    70  	transactionService transaction.Service
    71  
    72  	address      common.Address
    73  	contract     *chequebookContract
    74  	ownerAddress common.Address
    75  
    76  	erc20Service erc20.Service
    77  
    78  	store               storage.StateStorer
    79  	chequeSigner        ChequeSigner
    80  	totalIssuedReserved *big.Int
    81  }
    82  
    83  // New creates a new chequebook service for the provided chequebook contract.
    84  func New(transactionService transaction.Service, address, ownerAddress common.Address, store storage.StateStorer, chequeSigner ChequeSigner, erc20Service erc20.Service) (Service, error) {
    85  	return &service{
    86  		transactionService:  transactionService,
    87  		address:             address,
    88  		contract:            newChequebookContract(address, transactionService),
    89  		ownerAddress:        ownerAddress,
    90  		erc20Service:        erc20Service,
    91  		store:               store,
    92  		chequeSigner:        chequeSigner,
    93  		totalIssuedReserved: big.NewInt(0),
    94  	}, nil
    95  }
    96  
    97  // Address returns the address of the used chequebook contract.
    98  func (s *service) Address() common.Address {
    99  	return s.address
   100  }
   101  
   102  // Deposit starts depositing erc20 token into the chequebook. This returns once the transactions has been broadcast.
   103  func (s *service) Deposit(ctx context.Context, amount *big.Int) (hash common.Hash, err error) {
   104  	balance, err := s.erc20Service.BalanceOf(ctx, s.ownerAddress)
   105  	if err != nil {
   106  		return common.Hash{}, err
   107  	}
   108  
   109  	// check we can afford this so we don't waste gas
   110  	if balance.Cmp(amount) < 0 {
   111  		return common.Hash{}, ErrInsufficientFunds
   112  	}
   113  
   114  	return s.erc20Service.Transfer(ctx, s.address, amount)
   115  }
   116  
   117  // Balance returns the token balance of the chequebook.
   118  func (s *service) Balance(ctx context.Context) (*big.Int, error) {
   119  	return s.contract.Balance(ctx)
   120  }
   121  
   122  // AvailableBalance returns the token balance of the chequebook which is not yet used for uncashed cheques.
   123  func (s *service) AvailableBalance(ctx context.Context) (*big.Int, error) {
   124  	totalIssued, err := s.totalIssued()
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  
   129  	balance, err := s.Balance(ctx)
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  
   134  	totalPaidOut, err := s.contract.TotalPaidOut(ctx)
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  
   139  	// balance plus totalPaidOut is the total amount ever put into the chequebook (ignoring deposits and withdrawals which cancelled out)
   140  	// minus the total amount we issued from this chequebook this gives use the portion of the balance not covered by any cheques
   141  	availableBalance := big.NewInt(0).Add(balance, totalPaidOut)
   142  	availableBalance = availableBalance.Sub(availableBalance, totalIssued)
   143  	return availableBalance, nil
   144  }
   145  
   146  // WaitForDeposit waits for the deposit transaction to confirm and verifies the result.
   147  func (s *service) WaitForDeposit(ctx context.Context, txHash common.Hash) error {
   148  	receipt, err := s.transactionService.WaitForReceipt(ctx, txHash)
   149  	if err != nil {
   150  		return err
   151  	}
   152  	if receipt.Status != 1 {
   153  		return transaction.ErrTransactionReverted
   154  	}
   155  	return nil
   156  }
   157  
   158  // lastIssuedChequeKey computes the key where to store the last cheque for a beneficiary.
   159  func lastIssuedChequeKey(beneficiary common.Address) string {
   160  	return fmt.Sprintf("%s%x", lastIssuedChequeKeyPrefix, beneficiary)
   161  }
   162  
   163  func (s *service) reserveTotalIssued(ctx context.Context, amount *big.Int) (*big.Int, error) {
   164  	s.lock.Lock()
   165  	defer s.lock.Unlock()
   166  
   167  	availableBalance, err := s.AvailableBalance(ctx)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	if amount.Cmp(big.NewInt(0).Sub(availableBalance, s.totalIssuedReserved)) > 0 {
   173  		return nil, ErrOutOfFunds
   174  	}
   175  
   176  	s.totalIssuedReserved = s.totalIssuedReserved.Add(s.totalIssuedReserved, amount)
   177  	return big.NewInt(0).Sub(availableBalance, amount), nil
   178  }
   179  
   180  func (s *service) unreserveTotalIssued(amount *big.Int) {
   181  	s.lock.Lock()
   182  	defer s.lock.Unlock()
   183  	s.totalIssuedReserved = s.totalIssuedReserved.Sub(s.totalIssuedReserved, amount)
   184  }
   185  
   186  // Issue issues a new cheque and passes it to sendChequeFunc.
   187  // The cheque is considered sent and saved when sendChequeFunc succeeds.
   188  // The available balance which is available after sending the cheque is passed
   189  // to the caller for it to be communicated over metrics.
   190  func (s *service) Issue(ctx context.Context, beneficiary common.Address, amount *big.Int, sendChequeFunc SendChequeFunc) (*big.Int, error) {
   191  	availableBalance, err := s.reserveTotalIssued(ctx, amount)
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  	defer s.unreserveTotalIssued(amount)
   196  
   197  	var cumulativePayout *big.Int
   198  	lastCheque, err := s.LastCheque(beneficiary)
   199  	if err != nil {
   200  		if !errors.Is(err, ErrNoCheque) {
   201  			return nil, err
   202  		}
   203  		cumulativePayout = big.NewInt(0)
   204  	} else {
   205  		cumulativePayout = lastCheque.CumulativePayout
   206  	}
   207  
   208  	// increase cumulativePayout by amount
   209  	cumulativePayout = cumulativePayout.Add(cumulativePayout, amount)
   210  
   211  	// create and sign the new cheque
   212  	cheque := Cheque{
   213  		Chequebook:       s.address,
   214  		CumulativePayout: cumulativePayout,
   215  		Beneficiary:      beneficiary,
   216  	}
   217  
   218  	sig, err := s.chequeSigner.Sign(&Cheque{
   219  		Chequebook:       s.address,
   220  		CumulativePayout: cumulativePayout,
   221  		Beneficiary:      beneficiary,
   222  	})
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  
   227  	// actually send the check before saving to avoid double payment
   228  	err = sendChequeFunc(&SignedCheque{
   229  		Cheque:    cheque,
   230  		Signature: sig,
   231  	})
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  
   236  	err = s.store.Put(lastIssuedChequeKey(beneficiary), cheque)
   237  	if err != nil {
   238  		return nil, err
   239  	}
   240  
   241  	s.lock.Lock()
   242  	defer s.lock.Unlock()
   243  
   244  	totalIssued, err := s.totalIssued()
   245  	if err != nil {
   246  		return nil, err
   247  	}
   248  	totalIssued = totalIssued.Add(totalIssued, amount)
   249  	return availableBalance, s.store.Put(totalIssuedKey, totalIssued)
   250  }
   251  
   252  // returns the total amount in cheques issued so far
   253  func (s *service) totalIssued() (totalIssued *big.Int, err error) {
   254  	err = s.store.Get(totalIssuedKey, &totalIssued)
   255  	if err != nil {
   256  		if !errors.Is(err, storage.ErrNotFound) {
   257  			return nil, err
   258  		}
   259  		return big.NewInt(0), nil
   260  	}
   261  	return totalIssued, nil
   262  }
   263  
   264  // LastCheque returns the last cheque we issued for the beneficiary.
   265  func (s *service) LastCheque(beneficiary common.Address) (*SignedCheque, error) {
   266  	var lastCheque *SignedCheque
   267  	err := s.store.Get(lastIssuedChequeKey(beneficiary), &lastCheque)
   268  	if err != nil {
   269  		if !errors.Is(err, storage.ErrNotFound) {
   270  			return nil, err
   271  		}
   272  		return nil, ErrNoCheque
   273  	}
   274  	return lastCheque, nil
   275  }
   276  
   277  func keyBeneficiary(key []byte, prefix string) (beneficiary common.Address, err error) {
   278  	k := string(key)
   279  
   280  	split := strings.SplitAfter(k, prefix)
   281  	if len(split) != 2 {
   282  		return common.Address{}, errors.New("no beneficiary in key")
   283  	}
   284  	return common.HexToAddress(split[1]), nil
   285  }
   286  
   287  // LastCheque returns the last cheques for all beneficiaries.
   288  func (s *service) LastCheques() (map[common.Address]*SignedCheque, error) {
   289  	result := make(map[common.Address]*SignedCheque)
   290  	err := s.store.Iterate(lastIssuedChequeKeyPrefix, func(key, val []byte) (stop bool, err error) {
   291  		addr, err := keyBeneficiary(key, lastIssuedChequeKeyPrefix)
   292  		if err != nil {
   293  			return false, fmt.Errorf("parse address from key: %s: %w", string(key), err)
   294  		}
   295  
   296  		if _, ok := result[addr]; !ok {
   297  
   298  			lastCheque, err := s.LastCheque(addr)
   299  			if err != nil {
   300  				return false, err
   301  			}
   302  
   303  			result[addr] = lastCheque
   304  		}
   305  		return false, nil
   306  	})
   307  	if err != nil {
   308  		return nil, err
   309  	}
   310  	return result, nil
   311  }
   312  
   313  func (s *service) Withdraw(ctx context.Context, amount *big.Int) (hash common.Hash, err error) {
   314  	availableBalance, err := s.AvailableBalance(ctx)
   315  	if err != nil {
   316  		return common.Hash{}, err
   317  	}
   318  
   319  	// check we can afford this so we don't waste gas and don't risk bouncing cheques
   320  	if availableBalance.Cmp(amount) < 0 {
   321  		return common.Hash{}, ErrInsufficientFunds
   322  	}
   323  
   324  	callData, err := chequebookABI.Pack("withdraw", amount)
   325  	if err != nil {
   326  		return common.Hash{}, err
   327  	}
   328  
   329  	request := &transaction.TxRequest{
   330  		To:          &s.address,
   331  		Data:        callData,
   332  		GasPrice:    sctx.GetGasPrice(ctx),
   333  		GasLimit:    95000,
   334  		Value:       big.NewInt(0),
   335  		Description: fmt.Sprintf("chequebook withdrawal of %d BZZ", amount),
   336  	}
   337  
   338  	txHash, err := s.transactionService.Send(ctx, request, transaction.DefaultTipBoostPercent)
   339  	if err != nil {
   340  		return common.Hash{}, err
   341  	}
   342  
   343  	return txHash, nil
   344  }