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 }