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 }