github.com/ethersphere/bee/v2@v2.2.0/pkg/storageincentives/staking/contract.go (about)

     1  // Copyright 2022 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 staking
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"math/big"
    12  
    13  	"github.com/ethereum/go-ethereum/accounts/abi"
    14  	"github.com/ethereum/go-ethereum/common"
    15  	"github.com/ethereum/go-ethereum/core/types"
    16  	"github.com/ethersphere/bee/v2/pkg/sctx"
    17  	"github.com/ethersphere/bee/v2/pkg/transaction"
    18  	"github.com/ethersphere/bee/v2/pkg/util/abiutil"
    19  	"github.com/ethersphere/go-sw3-abi/sw3abi"
    20  )
    21  
    22  var (
    23  	MinimumStakeAmount = big.NewInt(100000000000000000)
    24  
    25  	erc20ABI = abiutil.MustParseABI(sw3abi.ERC20ABIv0_6_5)
    26  
    27  	ErrInsufficientStakeAmount = errors.New("insufficient stake amount")
    28  	ErrInsufficientFunds       = errors.New("insufficient token balance")
    29  	ErrInsufficientStake       = errors.New("insufficient stake")
    30  	ErrNotImplemented          = errors.New("not implemented")
    31  	ErrNotPaused               = errors.New("contract is not paused")
    32  	ErrUnexpectedLength        = errors.New("unexpected results length")
    33  
    34  	approveDescription       = "Approve tokens for stake deposit operations"
    35  	depositStakeDescription  = "Deposit Stake"
    36  	withdrawStakeDescription = "Withdraw stake"
    37  	migrateStakeDescription  = "Migrate stake"
    38  )
    39  
    40  type Contract interface {
    41  	DepositStake(ctx context.Context, stakedAmount *big.Int) (common.Hash, error)
    42  	ChangeStakeOverlay(ctx context.Context, nonce common.Hash) (common.Hash, error)
    43  	GetPotentialStake(ctx context.Context) (*big.Int, error)
    44  	GetWithdrawableStake(ctx context.Context) (*big.Int, error)
    45  	WithdrawStake(ctx context.Context) (common.Hash, error)
    46  	MigrateStake(ctx context.Context) (common.Hash, error)
    47  	RedistributionStatuser
    48  }
    49  
    50  type RedistributionStatuser interface {
    51  	IsOverlayFrozen(ctx context.Context, block uint64) (bool, error)
    52  }
    53  
    54  type contract struct {
    55  	owner                  common.Address
    56  	stakingContractAddress common.Address
    57  	stakingContractABI     abi.ABI
    58  	bzzTokenAddress        common.Address
    59  	transactionService     transaction.Service
    60  	overlayNonce           common.Hash
    61  	gasLimit               uint64
    62  }
    63  
    64  func New(
    65  	owner common.Address,
    66  	stakingContractAddress common.Address,
    67  	stakingContractABI abi.ABI,
    68  	bzzTokenAddress common.Address,
    69  	transactionService transaction.Service,
    70  	nonce common.Hash,
    71  	setGasLimit bool,
    72  ) Contract {
    73  
    74  	var gasLimit uint64
    75  	if setGasLimit {
    76  		gasLimit = transaction.DefaultGasLimit
    77  	}
    78  
    79  	return &contract{
    80  		owner:                  owner,
    81  		stakingContractAddress: stakingContractAddress,
    82  		stakingContractABI:     stakingContractABI,
    83  		bzzTokenAddress:        bzzTokenAddress,
    84  		transactionService:     transactionService,
    85  		overlayNonce:           nonce,
    86  		gasLimit:               gasLimit,
    87  	}
    88  }
    89  
    90  func (c *contract) DepositStake(ctx context.Context, stakedAmount *big.Int) (common.Hash, error) {
    91  	prevStakedAmount, err := c.GetPotentialStake(ctx)
    92  	if err != nil {
    93  		return common.Hash{}, err
    94  	}
    95  
    96  	if len(prevStakedAmount.Bits()) == 0 {
    97  		if stakedAmount.Cmp(MinimumStakeAmount) == -1 {
    98  			return common.Hash{}, ErrInsufficientStakeAmount
    99  		}
   100  	}
   101  
   102  	balance, err := c.getBalance(ctx)
   103  	if err != nil {
   104  		return common.Hash{}, err
   105  	}
   106  
   107  	if balance.Cmp(stakedAmount) < 0 {
   108  		return common.Hash{}, ErrInsufficientFunds
   109  	}
   110  
   111  	_, err = c.sendApproveTransaction(ctx, stakedAmount)
   112  	if err != nil {
   113  		return common.Hash{}, err
   114  	}
   115  
   116  	receipt, err := c.sendDepositStakeTransaction(ctx, stakedAmount, c.overlayNonce)
   117  	if err != nil {
   118  		return common.Hash{}, err
   119  	}
   120  
   121  	return receipt.TxHash, nil
   122  }
   123  
   124  // ChangeStakeOverlay only changes the overlay address used in the redistribution game.
   125  func (c *contract) ChangeStakeOverlay(ctx context.Context, nonce common.Hash) (common.Hash, error) {
   126  	c.overlayNonce = nonce
   127  	receipt, err := c.sendDepositStakeTransaction(ctx, new(big.Int), c.overlayNonce)
   128  	if err != nil {
   129  		return common.Hash{}, err
   130  	}
   131  
   132  	return receipt.TxHash, nil
   133  }
   134  
   135  func (c *contract) GetPotentialStake(ctx context.Context) (*big.Int, error) {
   136  	stakedAmount, err := c.getPotentialStake(ctx)
   137  	if err != nil {
   138  		return nil, fmt.Errorf("staking contract: failed to get stake: %w", err)
   139  	}
   140  	return stakedAmount, nil
   141  }
   142  
   143  func (c *contract) GetWithdrawableStake(ctx context.Context) (*big.Int, error) {
   144  	withdrawableStake, err := c.getWithdrawableStake(ctx)
   145  	if err != nil {
   146  		return nil, fmt.Errorf("staking contract: failed to get stake: %w", err)
   147  	}
   148  	return withdrawableStake, nil
   149  }
   150  
   151  func (c *contract) WithdrawStake(ctx context.Context) (txHash common.Hash, err error) {
   152  	withdrawableStake, err := c.getWithdrawableStake(ctx)
   153  	if err != nil {
   154  		return
   155  	}
   156  
   157  	if withdrawableStake.Cmp(big.NewInt(0)) <= 0 {
   158  		return common.Hash{}, ErrInsufficientStake
   159  	}
   160  
   161  	receipt, err := c.withdrawFromStake(ctx)
   162  	if err != nil {
   163  		return common.Hash{}, err
   164  	}
   165  	if receipt != nil {
   166  		txHash = receipt.TxHash
   167  	}
   168  	return txHash, nil
   169  }
   170  
   171  func (c *contract) MigrateStake(ctx context.Context) (txHash common.Hash, err error) {
   172  	isPaused, err := c.paused(ctx)
   173  	if err != nil {
   174  		return
   175  	}
   176  	if !isPaused {
   177  		return common.Hash{}, ErrNotPaused
   178  	}
   179  
   180  	receipt, err := c.migrateStake(ctx)
   181  	if err != nil {
   182  		return common.Hash{}, err
   183  	}
   184  	if receipt != nil {
   185  		txHash = receipt.TxHash
   186  	}
   187  	return txHash, nil
   188  }
   189  
   190  func (c *contract) IsOverlayFrozen(ctx context.Context, block uint64) (bool, error) {
   191  	callData, err := c.stakingContractABI.Pack("lastUpdatedBlockNumberOfAddress", c.owner)
   192  	if err != nil {
   193  		return false, err
   194  	}
   195  
   196  	result, err := c.transactionService.Call(ctx, &transaction.TxRequest{
   197  		To:   &c.stakingContractAddress,
   198  		Data: callData,
   199  	})
   200  	if err != nil {
   201  		return false, err
   202  	}
   203  
   204  	results, err := c.stakingContractABI.Unpack("lastUpdatedBlockNumberOfAddress", result)
   205  	if err != nil {
   206  		return false, err
   207  	}
   208  
   209  	if len(results) == 0 {
   210  		return false, errors.New("unexpected empty results")
   211  	}
   212  
   213  	lastUpdate := abi.ConvertType(results[0], new(big.Int)).(*big.Int)
   214  
   215  	return lastUpdate.Uint64() >= block, nil
   216  }
   217  
   218  func (c *contract) sendApproveTransaction(ctx context.Context, amount *big.Int) (receipt *types.Receipt, err error) {
   219  	callData, err := erc20ABI.Pack("approve", c.stakingContractAddress, amount)
   220  	if err != nil {
   221  		return nil, err
   222  	}
   223  
   224  	request := &transaction.TxRequest{
   225  		To:          &c.bzzTokenAddress,
   226  		Data:        callData,
   227  		GasPrice:    sctx.GetGasPrice(ctx),
   228  		GasLimit:    65000,
   229  		Value:       big.NewInt(0),
   230  		Description: approveDescription,
   231  	}
   232  
   233  	defer func() {
   234  		err = c.transactionService.UnwrapABIError(
   235  			ctx,
   236  			request,
   237  			err,
   238  			c.stakingContractABI.Errors,
   239  		)
   240  	}()
   241  
   242  	txHash, err := c.transactionService.Send(ctx, request, 0)
   243  	if err != nil {
   244  		return nil, err
   245  	}
   246  
   247  	receipt, err = c.transactionService.WaitForReceipt(ctx, txHash)
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  
   252  	if receipt.Status == 0 {
   253  		return nil, transaction.ErrTransactionReverted
   254  	}
   255  
   256  	return receipt, nil
   257  }
   258  
   259  func (c *contract) sendTransaction(ctx context.Context, callData []byte, desc string) (receipt *types.Receipt, err error) {
   260  	request := &transaction.TxRequest{
   261  		To:          &c.stakingContractAddress,
   262  		Data:        callData,
   263  		GasPrice:    sctx.GetGasPrice(ctx),
   264  		GasLimit:    max(sctx.GetGasLimit(ctx), c.gasLimit),
   265  		Value:       big.NewInt(0),
   266  		Description: desc,
   267  	}
   268  
   269  	defer func() {
   270  		err = c.transactionService.UnwrapABIError(
   271  			ctx,
   272  			request,
   273  			err,
   274  			c.stakingContractABI.Errors,
   275  		)
   276  	}()
   277  
   278  	txHash, err := c.transactionService.Send(ctx, request, transaction.DefaultTipBoostPercent)
   279  	if err != nil {
   280  		return nil, err
   281  	}
   282  
   283  	receipt, err = c.transactionService.WaitForReceipt(ctx, txHash)
   284  	if err != nil {
   285  		return nil, err
   286  	}
   287  
   288  	if receipt.Status == 0 {
   289  		return nil, transaction.ErrTransactionReverted
   290  	}
   291  
   292  	return receipt, nil
   293  }
   294  
   295  func (c *contract) sendDepositStakeTransaction(ctx context.Context, stakedAmount *big.Int, nonce common.Hash) (*types.Receipt, error) {
   296  	callData, err := c.stakingContractABI.Pack("manageStake", nonce, stakedAmount)
   297  	if err != nil {
   298  		return nil, err
   299  	}
   300  
   301  	receipt, err := c.sendTransaction(ctx, callData, depositStakeDescription)
   302  	if err != nil {
   303  		return nil, fmt.Errorf("deposit stake: stakedAmount %d: %w", stakedAmount, err)
   304  	}
   305  
   306  	return receipt, nil
   307  }
   308  
   309  func (c *contract) getPotentialStake(ctx context.Context) (*big.Int, error) {
   310  	callData, err := c.stakingContractABI.Pack("stakes", c.owner)
   311  	if err != nil {
   312  		return nil, err
   313  	}
   314  	result, err := c.transactionService.Call(ctx, &transaction.TxRequest{
   315  		To:   &c.stakingContractAddress,
   316  		Data: callData,
   317  	})
   318  	if err != nil {
   319  		return nil, fmt.Errorf("get potential stake: %w", err)
   320  	}
   321  
   322  	// overlay bytes32,
   323  	// committedStake uint256,
   324  	// potentialStake uint256,
   325  	// lastUpdatedBlockNumber uint256,
   326  	results, err := c.stakingContractABI.Unpack("stakes", result)
   327  	if err != nil {
   328  		return nil, err
   329  	}
   330  
   331  	if len(results) < 4 {
   332  		return nil, ErrUnexpectedLength
   333  	}
   334  
   335  	return abi.ConvertType(results[2], new(big.Int)).(*big.Int), nil
   336  }
   337  
   338  func (c *contract) getWithdrawableStake(ctx context.Context) (*big.Int, error) {
   339  	callData, err := c.stakingContractABI.Pack("withdrawableStake")
   340  	if err != nil {
   341  		return nil, err
   342  	}
   343  	result, err := c.transactionService.Call(ctx, &transaction.TxRequest{
   344  		To:   &c.stakingContractAddress,
   345  		Data: callData,
   346  	})
   347  	if err != nil {
   348  		return nil, fmt.Errorf("get withdrawable stake: %w", err)
   349  	}
   350  
   351  	results, err := c.stakingContractABI.Unpack("withdrawableStake", result)
   352  	if err != nil {
   353  		return nil, err
   354  	}
   355  
   356  	if len(results) == 0 {
   357  		return nil, errors.New("unexpected empty results")
   358  	}
   359  
   360  	return abi.ConvertType(results[0], new(big.Int)).(*big.Int), nil
   361  }
   362  
   363  func (c *contract) getBalance(ctx context.Context) (*big.Int, error) {
   364  	callData, err := erc20ABI.Pack("balanceOf", c.owner)
   365  	if err != nil {
   366  		return nil, err
   367  	}
   368  
   369  	result, err := c.transactionService.Call(ctx, &transaction.TxRequest{
   370  		To:   &c.bzzTokenAddress,
   371  		Data: callData,
   372  	})
   373  	if err != nil {
   374  		return nil, err
   375  	}
   376  
   377  	results, err := erc20ABI.Unpack("balanceOf", result)
   378  	if err != nil {
   379  		return nil, err
   380  	}
   381  
   382  	if len(results) == 0 {
   383  		return nil, errors.New("unexpected empty results")
   384  	}
   385  
   386  	return abi.ConvertType(results[0], new(big.Int)).(*big.Int), nil
   387  }
   388  
   389  func (c *contract) migrateStake(ctx context.Context) (*types.Receipt, error) {
   390  	callData, err := c.stakingContractABI.Pack("migrateStake")
   391  	if err != nil {
   392  		return nil, err
   393  	}
   394  
   395  	receipt, err := c.sendTransaction(ctx, callData, migrateStakeDescription)
   396  	if err != nil {
   397  		return nil, fmt.Errorf("migrate stake: %w", err)
   398  	}
   399  
   400  	return receipt, nil
   401  }
   402  
   403  func (c *contract) withdrawFromStake(ctx context.Context) (*types.Receipt, error) {
   404  	callData, err := c.stakingContractABI.Pack("withdrawFromStake")
   405  	if err != nil {
   406  		return nil, err
   407  	}
   408  
   409  	receipt, err := c.sendTransaction(ctx, callData, withdrawStakeDescription)
   410  	if err != nil {
   411  		return nil, fmt.Errorf("withdraw stake: %w", err)
   412  	}
   413  
   414  	return receipt, nil
   415  }
   416  
   417  func (c *contract) paused(ctx context.Context) (bool, error) {
   418  	callData, err := c.stakingContractABI.Pack("paused")
   419  	if err != nil {
   420  		return false, err
   421  	}
   422  
   423  	result, err := c.transactionService.Call(ctx, &transaction.TxRequest{
   424  		To:   &c.stakingContractAddress,
   425  		Data: callData,
   426  	})
   427  	if err != nil {
   428  		return false, err
   429  	}
   430  
   431  	results, err := c.stakingContractABI.Unpack("paused", result)
   432  	if err != nil {
   433  		return false, err
   434  	}
   435  
   436  	if len(results) == 0 {
   437  		return false, errors.New("unexpected empty results")
   438  	}
   439  
   440  	return results[0].(bool), nil
   441  }