github.com/ethersphere/bee/v2@v2.2.0/pkg/postage/postagecontract/contract.go (about)

     1  // Copyright 2021 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 postagecontract
     6  
     7  import (
     8  	"context"
     9  	"crypto/rand"
    10  	"errors"
    11  	"fmt"
    12  	"math/big"
    13  
    14  	"github.com/ethereum/go-ethereum/accounts/abi"
    15  	"github.com/ethereum/go-ethereum/common"
    16  	"github.com/ethereum/go-ethereum/core/types"
    17  	"github.com/ethersphere/bee/v2/pkg/postage"
    18  	"github.com/ethersphere/bee/v2/pkg/sctx"
    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  var (
    25  	BucketDepth = uint8(16)
    26  
    27  	erc20ABI = abiutil.MustParseABI(sw3abi.ERC20ABIv0_6_5)
    28  
    29  	ErrBatchCreate          = errors.New("batch creation failed")
    30  	ErrInsufficientFunds    = errors.New("insufficient token balance")
    31  	ErrInvalidDepth         = errors.New("invalid depth")
    32  	ErrBatchTopUp           = errors.New("batch topUp failed")
    33  	ErrBatchDilute          = errors.New("batch dilute failed")
    34  	ErrChainDisabled        = errors.New("chain disabled")
    35  	ErrNotImplemented       = errors.New("not implemented")
    36  	ErrInsufficientValidity = errors.New("insufficient validity")
    37  
    38  	approveDescription     = "Approve tokens for postage operations"
    39  	createBatchDescription = "Postage batch creation"
    40  	topUpBatchDescription  = "Postage batch top up"
    41  	diluteBatchDescription = "Postage batch dilute"
    42  )
    43  
    44  type Interface interface {
    45  	CreateBatch(ctx context.Context, initialBalance *big.Int, depth uint8, immutable bool, label string) (common.Hash, []byte, error)
    46  	TopUpBatch(ctx context.Context, batchID []byte, topupBalance *big.Int) (common.Hash, error)
    47  	DiluteBatch(ctx context.Context, batchID []byte, newDepth uint8) (common.Hash, error)
    48  	Paused(ctx context.Context) (bool, error)
    49  	PostageBatchExpirer
    50  }
    51  
    52  type PostageBatchExpirer interface {
    53  	ExpireBatches(ctx context.Context) error
    54  }
    55  
    56  type postageContract struct {
    57  	owner                       common.Address
    58  	postageStampContractAddress common.Address
    59  	postageStampContractABI     abi.ABI
    60  	bzzTokenAddress             common.Address
    61  	transactionService          transaction.Service
    62  	postageService              postage.Service
    63  	postageStorer               postage.Storer
    64  
    65  	// Cached postage stamp contract event topics.
    66  	batchCreatedTopic       common.Hash
    67  	batchTopUpTopic         common.Hash
    68  	batchDepthIncreaseTopic common.Hash
    69  
    70  	gasLimit uint64
    71  }
    72  
    73  func New(
    74  	owner common.Address,
    75  	postageStampContractAddress common.Address,
    76  	postageStampContractABI abi.ABI,
    77  	bzzTokenAddress common.Address,
    78  	transactionService transaction.Service,
    79  	postageService postage.Service,
    80  	postageStorer postage.Storer,
    81  	chainEnabled bool,
    82  	setGasLimit bool,
    83  ) Interface {
    84  	if !chainEnabled {
    85  		return new(noOpPostageContract)
    86  	}
    87  
    88  	var gasLimit uint64
    89  	if setGasLimit {
    90  		gasLimit = transaction.DefaultGasLimit
    91  	}
    92  
    93  	return &postageContract{
    94  		owner:                       owner,
    95  		postageStampContractAddress: postageStampContractAddress,
    96  		postageStampContractABI:     postageStampContractABI,
    97  		bzzTokenAddress:             bzzTokenAddress,
    98  		transactionService:          transactionService,
    99  		postageService:              postageService,
   100  		postageStorer:               postageStorer,
   101  
   102  		batchCreatedTopic:       postageStampContractABI.Events["BatchCreated"].ID,
   103  		batchTopUpTopic:         postageStampContractABI.Events["BatchTopUp"].ID,
   104  		batchDepthIncreaseTopic: postageStampContractABI.Events["BatchDepthIncrease"].ID,
   105  
   106  		gasLimit: gasLimit,
   107  	}
   108  }
   109  
   110  func (c *postageContract) ExpireBatches(ctx context.Context) error {
   111  	for {
   112  		exists, err := c.expiredBatchesExists(ctx)
   113  		if err != nil {
   114  			return fmt.Errorf("expired batches exist: %w", err)
   115  		}
   116  		if !exists {
   117  			break
   118  		}
   119  
   120  		err = c.expireLimitedBatches(ctx, big.NewInt(25))
   121  		if err != nil {
   122  			return fmt.Errorf("expire limited batches: %w", err)
   123  		}
   124  	}
   125  	return nil
   126  }
   127  
   128  func (c *postageContract) expiredBatchesExists(ctx context.Context) (bool, error) {
   129  	callData, err := c.postageStampContractABI.Pack("expiredBatchesExist")
   130  	if err != nil {
   131  		return false, err
   132  	}
   133  
   134  	result, err := c.transactionService.Call(ctx, &transaction.TxRequest{
   135  		To:   &c.postageStampContractAddress,
   136  		Data: callData,
   137  	})
   138  	if err != nil {
   139  		return false, err
   140  	}
   141  
   142  	results, err := c.postageStampContractABI.Unpack("expiredBatchesExist", result)
   143  	if err != nil {
   144  		return false, err
   145  	}
   146  	return results[0].(bool), nil
   147  }
   148  
   149  func (c *postageContract) expireLimitedBatches(ctx context.Context, count *big.Int) error {
   150  	callData, err := c.postageStampContractABI.Pack("expireLimited", count)
   151  	if err != nil {
   152  		return err
   153  	}
   154  
   155  	_, err = c.sendTransaction(ctx, callData, "expire limited batches")
   156  	if err != nil {
   157  		return err
   158  	}
   159  
   160  	return nil
   161  }
   162  
   163  func (c *postageContract) sendApproveTransaction(ctx context.Context, amount *big.Int) (receipt *types.Receipt, err error) {
   164  	callData, err := erc20ABI.Pack("approve", c.postageStampContractAddress, amount)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  
   169  	request := &transaction.TxRequest{
   170  		To:          &c.bzzTokenAddress,
   171  		Data:        callData,
   172  		GasPrice:    sctx.GetGasPrice(ctx),
   173  		GasLimit:    65000,
   174  		Value:       big.NewInt(0),
   175  		Description: approveDescription,
   176  	}
   177  
   178  	defer func() {
   179  		err = c.transactionService.UnwrapABIError(
   180  			ctx,
   181  			request,
   182  			err,
   183  			c.postageStampContractABI.Errors,
   184  		)
   185  	}()
   186  
   187  	txHash, err := c.transactionService.Send(ctx, request, transaction.DefaultTipBoostPercent)
   188  	if err != nil {
   189  		return nil, err
   190  	}
   191  
   192  	receipt, err = c.transactionService.WaitForReceipt(ctx, txHash)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  
   197  	if receipt.Status == 0 {
   198  		return nil, transaction.ErrTransactionReverted
   199  	}
   200  
   201  	return receipt, nil
   202  }
   203  
   204  func (c *postageContract) sendTransaction(ctx context.Context, callData []byte, desc string) (receipt *types.Receipt, err error) {
   205  	request := &transaction.TxRequest{
   206  		To:          &c.postageStampContractAddress,
   207  		Data:        callData,
   208  		GasPrice:    sctx.GetGasPrice(ctx),
   209  		GasLimit:    max(sctx.GetGasLimit(ctx), c.gasLimit),
   210  		Value:       big.NewInt(0),
   211  		Description: desc,
   212  	}
   213  
   214  	defer func() {
   215  		err = c.transactionService.UnwrapABIError(
   216  			ctx,
   217  			request,
   218  			err,
   219  			c.postageStampContractABI.Errors,
   220  		)
   221  	}()
   222  
   223  	txHash, err := c.transactionService.Send(ctx, request, transaction.DefaultTipBoostPercent)
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  
   228  	receipt, err = c.transactionService.WaitForReceipt(ctx, txHash)
   229  	if err != nil {
   230  		return nil, err
   231  	}
   232  
   233  	if receipt.Status == 0 {
   234  		return nil, transaction.ErrTransactionReverted
   235  	}
   236  
   237  	return receipt, nil
   238  }
   239  
   240  func (c *postageContract) sendCreateBatchTransaction(ctx context.Context, owner common.Address, initialBalance *big.Int, depth uint8, nonce common.Hash, immutable bool) (*types.Receipt, error) {
   241  
   242  	callData, err := c.postageStampContractABI.Pack("createBatch", owner, initialBalance, depth, BucketDepth, nonce, immutable)
   243  	if err != nil {
   244  		return nil, err
   245  	}
   246  
   247  	receipt, err := c.sendTransaction(ctx, callData, createBatchDescription)
   248  	if err != nil {
   249  		return nil, fmt.Errorf("create batch: depth %d bucketDepth %d immutable %t: %w", depth, BucketDepth, immutable, err)
   250  	}
   251  
   252  	return receipt, nil
   253  }
   254  
   255  func (c *postageContract) sendTopUpBatchTransaction(ctx context.Context, batchID []byte, topUpAmount *big.Int) (*types.Receipt, error) {
   256  
   257  	callData, err := c.postageStampContractABI.Pack("topUp", common.BytesToHash(batchID), topUpAmount)
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  
   262  	receipt, err := c.sendTransaction(ctx, callData, topUpBatchDescription)
   263  	if err != nil {
   264  		return nil, fmt.Errorf("topup batch: amount %d: %w", topUpAmount.Int64(), err)
   265  	}
   266  
   267  	return receipt, nil
   268  }
   269  
   270  func (c *postageContract) sendDiluteTransaction(ctx context.Context, batchID []byte, newDepth uint8) (*types.Receipt, error) {
   271  
   272  	callData, err := c.postageStampContractABI.Pack("increaseDepth", common.BytesToHash(batchID), newDepth)
   273  	if err != nil {
   274  		return nil, err
   275  	}
   276  
   277  	receipt, err := c.sendTransaction(ctx, callData, diluteBatchDescription)
   278  	if err != nil {
   279  		return nil, fmt.Errorf("dilute batch: new depth %d: %w", newDepth, err)
   280  	}
   281  
   282  	return receipt, nil
   283  }
   284  
   285  func (c *postageContract) getBalance(ctx context.Context) (*big.Int, error) {
   286  	callData, err := erc20ABI.Pack("balanceOf", c.owner)
   287  	if err != nil {
   288  		return nil, err
   289  	}
   290  
   291  	result, err := c.transactionService.Call(ctx, &transaction.TxRequest{
   292  		To:   &c.bzzTokenAddress,
   293  		Data: callData,
   294  	})
   295  	if err != nil {
   296  		return nil, err
   297  	}
   298  
   299  	results, err := erc20ABI.Unpack("balanceOf", result)
   300  	if err != nil {
   301  		return nil, err
   302  	}
   303  	return abi.ConvertType(results[0], new(big.Int)).(*big.Int), nil
   304  }
   305  
   306  func (c *postageContract) getProperty(ctx context.Context, propertyName string, out any) error {
   307  	callData, err := c.postageStampContractABI.Pack(propertyName)
   308  	if err != nil {
   309  		return err
   310  	}
   311  
   312  	result, err := c.transactionService.Call(ctx, &transaction.TxRequest{
   313  		To:   &c.postageStampContractAddress,
   314  		Data: callData,
   315  	})
   316  	if err != nil {
   317  		return err
   318  	}
   319  
   320  	results, err := c.postageStampContractABI.Unpack(propertyName, result)
   321  	if err != nil {
   322  		return err
   323  	}
   324  
   325  	if len(results) == 0 {
   326  		return errors.New("unexpected empty results")
   327  	}
   328  
   329  	abi.ConvertType(results[0], out)
   330  	return nil
   331  }
   332  
   333  func (c *postageContract) getMinInitialBalance(ctx context.Context) (uint64, error) {
   334  	var lastPrice uint64
   335  	err := c.getProperty(ctx, "lastPrice", &lastPrice)
   336  	if err != nil {
   337  		return 0, err
   338  	}
   339  	var minimumValidityBlocks uint64
   340  	err = c.getProperty(ctx, "minimumValidityBlocks", &minimumValidityBlocks)
   341  	if err != nil {
   342  		return 0, err
   343  	}
   344  	return lastPrice * minimumValidityBlocks, nil
   345  }
   346  
   347  func (c *postageContract) CreateBatch(ctx context.Context, initialBalance *big.Int, depth uint8, immutable bool, label string) (txHash common.Hash, batchID []byte, err error) {
   348  	if depth <= BucketDepth {
   349  		err = ErrInvalidDepth
   350  		return
   351  	}
   352  
   353  	totalAmount := big.NewInt(0).Mul(initialBalance, big.NewInt(int64(1<<depth)))
   354  	balance, err := c.getBalance(ctx)
   355  	if err != nil {
   356  		return
   357  	}
   358  
   359  	if balance.Cmp(totalAmount) < 0 {
   360  		err = fmt.Errorf("insufficient balance. amount %d, balance %d: %w", totalAmount, balance, ErrInsufficientFunds)
   361  		return
   362  	}
   363  
   364  	minInitialBalance, err := c.getMinInitialBalance(ctx)
   365  	if err != nil {
   366  		return
   367  	}
   368  	if initialBalance.Cmp(big.NewInt(int64(minInitialBalance))) <= 0 {
   369  		err = fmt.Errorf("insufficient initial balance for 24h minimum validity. balance %d, minimum amount: %d: %w", initialBalance, minInitialBalance, ErrInsufficientValidity)
   370  		return
   371  	}
   372  
   373  	err = c.ExpireBatches(ctx)
   374  	if err != nil {
   375  		return
   376  	}
   377  
   378  	_, err = c.sendApproveTransaction(ctx, totalAmount)
   379  	if err != nil {
   380  		return
   381  	}
   382  
   383  	nonce := make([]byte, 32)
   384  	_, err = rand.Read(nonce)
   385  	if err != nil {
   386  		return
   387  	}
   388  
   389  	receipt, err := c.sendCreateBatchTransaction(ctx, c.owner, initialBalance, depth, common.BytesToHash(nonce), immutable)
   390  	if err != nil {
   391  		return
   392  	}
   393  	txHash = receipt.TxHash
   394  	for _, ev := range receipt.Logs {
   395  		if ev.Address == c.postageStampContractAddress && len(ev.Topics) > 0 && ev.Topics[0] == c.batchCreatedTopic {
   396  			var createdEvent batchCreatedEvent
   397  			err = transaction.ParseEvent(&c.postageStampContractABI, "BatchCreated", &createdEvent, *ev)
   398  
   399  			if err != nil {
   400  				return
   401  			}
   402  
   403  			batchID = createdEvent.BatchId[:]
   404  			err = c.postageService.Add(postage.NewStampIssuer(
   405  				label,
   406  				c.owner.Hex(),
   407  				batchID,
   408  				initialBalance,
   409  				createdEvent.Depth,
   410  				createdEvent.BucketDepth,
   411  				ev.BlockNumber,
   412  				createdEvent.ImmutableFlag,
   413  			))
   414  
   415  			if err != nil {
   416  				return
   417  			}
   418  			return
   419  		}
   420  	}
   421  	err = ErrBatchCreate
   422  	return
   423  }
   424  
   425  func (c *postageContract) TopUpBatch(ctx context.Context, batchID []byte, topupBalance *big.Int) (txHash common.Hash, err error) {
   426  
   427  	batch, err := c.postageStorer.Get(batchID)
   428  	if err != nil {
   429  		return
   430  	}
   431  
   432  	totalAmount := big.NewInt(0).Mul(topupBalance, big.NewInt(int64(1<<batch.Depth)))
   433  	balance, err := c.getBalance(ctx)
   434  	if err != nil {
   435  		return
   436  	}
   437  
   438  	if balance.Cmp(totalAmount) < 0 {
   439  		err = ErrInsufficientFunds
   440  		return
   441  	}
   442  
   443  	_, err = c.sendApproveTransaction(ctx, totalAmount)
   444  	if err != nil {
   445  		return
   446  	}
   447  
   448  	receipt, err := c.sendTopUpBatchTransaction(ctx, batch.ID, topupBalance)
   449  	if err != nil {
   450  		txHash = receipt.TxHash
   451  		return
   452  	}
   453  
   454  	for _, ev := range receipt.Logs {
   455  		if ev.Address == c.postageStampContractAddress && len(ev.Topics) > 0 && ev.Topics[0] == c.batchTopUpTopic {
   456  			txHash = receipt.TxHash
   457  			return
   458  		}
   459  	}
   460  
   461  	err = ErrBatchTopUp
   462  	return
   463  }
   464  
   465  func (c *postageContract) DiluteBatch(ctx context.Context, batchID []byte, newDepth uint8) (txHash common.Hash, err error) {
   466  
   467  	batch, err := c.postageStorer.Get(batchID)
   468  	if err != nil {
   469  		return
   470  	}
   471  
   472  	if batch.Depth > newDepth {
   473  		err = fmt.Errorf("new depth should be greater: %w", ErrInvalidDepth)
   474  		return
   475  	}
   476  
   477  	err = c.ExpireBatches(ctx)
   478  	if err != nil {
   479  		return
   480  	}
   481  
   482  	receipt, err := c.sendDiluteTransaction(ctx, batch.ID, newDepth)
   483  	if err != nil {
   484  		return
   485  	}
   486  	txHash = receipt.TxHash
   487  	for _, ev := range receipt.Logs {
   488  		if ev.Address == c.postageStampContractAddress && len(ev.Topics) > 0 && ev.Topics[0] == c.batchDepthIncreaseTopic {
   489  			return
   490  		}
   491  	}
   492  	err = ErrBatchDilute
   493  	return
   494  }
   495  
   496  func (c *postageContract) Paused(ctx context.Context) (bool, error) {
   497  	callData, err := c.postageStampContractABI.Pack("paused")
   498  	if err != nil {
   499  		return false, err
   500  	}
   501  
   502  	result, err := c.transactionService.Call(ctx, &transaction.TxRequest{
   503  		To:   &c.postageStampContractAddress,
   504  		Data: callData,
   505  	})
   506  	if err != nil {
   507  		return false, err
   508  	}
   509  
   510  	results, err := c.postageStampContractABI.Unpack("paused", result)
   511  	if err != nil {
   512  		return false, err
   513  	}
   514  
   515  	if len(results) == 0 {
   516  		return false, errors.New("unexpected empty results")
   517  	}
   518  
   519  	return results[0].(bool), nil
   520  }
   521  
   522  type batchCreatedEvent struct {
   523  	BatchId           [32]byte
   524  	TotalAmount       *big.Int
   525  	NormalisedBalance *big.Int
   526  	Owner             common.Address
   527  	Depth             uint8
   528  	BucketDepth       uint8
   529  	ImmutableFlag     bool
   530  }
   531  
   532  type noOpPostageContract struct{}
   533  
   534  func (m *noOpPostageContract) CreateBatch(context.Context, *big.Int, uint8, bool, string) (common.Hash, []byte, error) {
   535  	return common.Hash{}, nil, nil
   536  }
   537  func (m *noOpPostageContract) TopUpBatch(context.Context, []byte, *big.Int) (common.Hash, error) {
   538  	return common.Hash{}, ErrChainDisabled
   539  }
   540  func (m *noOpPostageContract) DiluteBatch(context.Context, []byte, uint8) (common.Hash, error) {
   541  	return common.Hash{}, ErrChainDisabled
   542  }
   543  
   544  func (m *noOpPostageContract) Paused(context.Context) (bool, error) {
   545  	return false, nil
   546  }
   547  
   548  func (m *noOpPostageContract) ExpireBatches(context.Context) error {
   549  	return ErrChainDisabled
   550  }
   551  
   552  func LookupERC20Address(ctx context.Context, transactionService transaction.Service, postageStampContractAddress common.Address, postageStampContractABI abi.ABI, chainEnabled bool) (common.Address, error) {
   553  	if !chainEnabled {
   554  		return common.Address{}, nil
   555  	}
   556  
   557  	callData, err := postageStampContractABI.Pack("bzzToken")
   558  	if err != nil {
   559  		return common.Address{}, err
   560  	}
   561  
   562  	request := &transaction.TxRequest{
   563  		To:       &postageStampContractAddress,
   564  		Data:     callData,
   565  		GasPrice: nil,
   566  		GasLimit: 0,
   567  		Value:    big.NewInt(0),
   568  	}
   569  
   570  	data, err := transactionService.Call(ctx, request)
   571  	if err != nil {
   572  		return common.Address{}, err
   573  	}
   574  
   575  	return common.BytesToAddress(data), nil
   576  }