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 }