From eccf82ab9d06db92ca952bf4ef309b7eb0b62065 Mon Sep 17 00:00:00 2001 From: Dzmitry Aliksandrau Date: Wed, 30 Nov 2022 16:11:14 +0400 Subject: [PATCH] Read bulk_payout logs to avoid double payouts --- hmt_escrow/eth_bridge.py | 42 +++++++++++++++++++++++++++++++++--- hmt_escrow/job.py | 43 +++++++++++++++++++++++++++++++++---- package-lock.json | 2 +- package.json | 2 +- setup.py | 2 +- test/hmt_escrow/test_job.py | 34 +++++++++++++++++++++++------ 6 files changed, 109 insertions(+), 16 deletions(-) diff --git a/hmt_escrow/eth_bridge.py b/hmt_escrow/eth_bridge.py index 72553857..c50a8463 100644 --- a/hmt_escrow/eth_bridge.py +++ b/hmt_escrow/eth_bridge.py @@ -200,6 +200,19 @@ def get_contract_interface(contract_entrypoint): return contract_interface +def get_hmtoken_interface(): + """Retrieve the HMToken interface. + + Returns: + Contract interface: returns the HMToken interface solidity contract. + + """ + + return get_contract_interface( + "{}/HMTokenInterface.sol:HMTokenInterface".format(CONTRACT_FOLDER) + ) + + def get_hmtoken(hmtoken_addr=HMTOKEN_ADDR, hmt_server_addr: str = None) -> Contract: """Retrieve the HMToken contract from a given address. @@ -214,9 +227,7 @@ def get_hmtoken(hmtoken_addr=HMTOKEN_ADDR, hmt_server_addr: str = None) -> Contr """ w3 = get_w3(hmt_server_addr) - contract_interface = get_contract_interface( - "{}/HMTokenInterface.sol:HMTokenInterface".format(CONTRACT_FOLDER) - ) + contract_interface = get_hmtoken_interface() contract = w3.eth.contract(address=hmtoken_addr, abi=contract_interface["abi"]) return contract @@ -437,3 +448,28 @@ def set_pub_key_at_addr( } return handle_transaction(txn_func, *func_args, **txn_info) + + +def get_entity_topic(contract_interface: Dict, name: str) -> str: + """ + Args: + contract_interface (Dict): contract inteface. + + name (str): event name to find in abi. + + Returns + str: returns keccak_256 hash of event name with input parameters. + """ + s = "" + + for entity in contract_interface["abi"]: + event_name = entity.get("name") + if event_name == name: + s += event_name + "(" + inputs = entity.get("inputs", []) + input_types = [] + for input in inputs: + input_types.append(input.get("internalType")) + s += ",".join(input_types) + ")" + + return Web3.keccak(text=s) diff --git a/hmt_escrow/job.py b/hmt_escrow/job.py index 138b3f36..7ffe047b 100644 --- a/hmt_escrow/job.py +++ b/hmt_escrow/job.py @@ -15,6 +15,8 @@ from hmt_escrow import utils from hmt_escrow.eth_bridge import ( get_hmtoken, + get_hmtoken_interface, + get_entity_topic, get_escrow, get_factory, deploy_factory, @@ -26,6 +28,7 @@ from hmt_escrow.storage import download, upload, get_public_bucket_url, get_key_from_url GAS_LIMIT = int(os.getenv("GAS_LIMIT", 4712388)) +TRANSFER_EVENT = get_entity_topic(get_hmtoken_interface(), "Transfer") # Explicit env variable that will use s3 for storing results. @@ -617,6 +620,7 @@ def bulk_payout( bool: returns True if paying to ethereum addresses and oracles succeeds. """ + bulk_paid = False txn_event = "Bulk payout" txn_func = self.job_contract.functions.bulkPayOut txn_info = { @@ -646,23 +650,35 @@ def bulk_payout( func_args = [eth_addrs, hmt_amounts, url, hash_, 1] try: - handle_transaction_with_retry(txn_func, self.retry, *func_args, **txn_info) - return self._bulk_paid() is True + tx_receipt = handle_transaction_with_retry( + txn_func, + self.retry, + *func_args, + **txn_info, + ) + bulk_paid = self._check_transfer_event(tx_receipt) except Exception as e: LOG.warn( f"{txn_event} failed with main credentials: {self.gas_payer}, {self.gas_payer_priv} due to {e}. Using secondary ones..." ) + if bulk_paid: + return bulk_paid + + LOG.warn( + f"{txn_event} failed with main credentials: {self.gas_payer}, {self.gas_payer_priv}. Using secondary ones..." + ) + raffle_txn_res = self._raffle_txn( self.multi_credentials, txn_func, func_args, txn_event ) - bulk_paid = raffle_txn_res["txn_succeeded"] + bulk_paid = self._check_transfer_event(raffle_txn_res["tx_receipt"]) if not bulk_paid: LOG.warning(f"{txn_event} failed with all credentials.") - return bulk_paid is True + return bulk_paid def abort(self) -> bool: """Kills the contract and returns the HMT back to the gas payer. @@ -1508,3 +1524,22 @@ def _raffle_txn(self, multi_creds, txn_func, txn_args, txn_event) -> RaffleTxn: ) return {"txn_succeeded": txn_succeeded, "tx_receipt": tx_receipt} + + def _check_transfer_event(self, tx_receipt: Optional[TxReceipt]) -> bool: + """ + Check if transaction receipt has bulkTransfer event, to make sure that transaction was successful. + + Args: + tx_receipt (Optional[TxReceipt]): a dict with transaction receipt. + + Returns: + bool: returns True if transaction has bulkTransfer event, otherwise returns False. + """ + if not tx_receipt: + return False + + for log in tx_receipt.get("logs", {}): + for topic in log["topics"]: + if TRANSFER_EVENT == topic: + return True + return False diff --git a/package-lock.json b/package-lock.json index 77ab31b0..b3a7ecad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hmt-escrow", - "version": "0.14.6", + "version": "0.14.7", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4d600221..fa50573a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hmt-escrow", - "version": "0.14.6", + "version": "0.14.7", "description": "Launch escrow contracts to the HUMAN network", "main": "truffle.js", "directories": { diff --git a/setup.py b/setup.py index aee94bfb..d8ab2633 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="hmt-escrow", - version="0.14.6", + version="0.14.7", author="HUMAN Protocol", description="A python library to launch escrow contracts to the HUMAN network.", url="https://github.com/humanprotocol/hmt-escrow", diff --git a/test/hmt_escrow/test_job.py b/test/hmt_escrow/test_job.py index c8b3aaae..9418b4c9 100644 --- a/test/hmt_escrow/test_job.py +++ b/test/hmt_escrow/test_job.py @@ -225,8 +225,8 @@ def test_job_bulk_payout(self): ] self.assertTrue(self.job.bulk_payout(payouts, {}, self.rep_oracle_pub_key)) - def test_job_bulk_payout_with_encryption_option(self): - """Tests whether final results must be persisted in storage encrypted or plain.""" + def test_job_bulk_payout_with_false_encryption_option(self): + """Test that final results are stored encrypted""" job = Job(self.credentials, manifest) self.assertEqual(job.launch(self.rep_oracle_pub_key), True) self.assertEqual(job.setup(), True) @@ -253,10 +253,24 @@ def test_job_bulk_payout_with_encryption_option(self): encrypt_data=False, use_public_bucket=False, ) - mock_upload.reset_mock() - # Testing option as: encrypt final results: encrypt_final_results=True + def test_job_bulk_payout_with_true_encryption_option(self): + """Test that final results are stored uncrypted""" + job = Job(self.credentials, manifest) + self.assertEqual(job.launch(self.rep_oracle_pub_key), True) + self.assertEqual(job.setup(), True) + + payouts = [("0x852023fbb19050B8291a335E5A83Ac9701E7B4E6", Decimal("100.0"))] + + final_results = {"results": 0} + + mock_upload = MagicMock(return_value=("hash", "url")) + + # Testing option as: encrypt final results: encrypt_final_results=True + with patch("hmt_escrow.job.upload") as mock_upload: # Bulk payout with final results as plain (not encrypted) + mock_upload.return_value = ("hash", "url") + job.bulk_payout( payouts=payouts, results=final_results, @@ -325,15 +339,19 @@ def test_job_bulk_payout_with_full_qualified_url(self): self.assertEqual(job.setup(), True) payouts = [("0x852023fbb19050B8291a335E5A83Ac9701E7B4E6", Decimal("100.0"))] - final_results = {"results": 0} with patch( "hmt_escrow.job.handle_transaction_with_retry" - ) as transaction_retry_mock, patch("hmt_escrow.job.upload") as upload_mock: + ) as transaction_retry_mock, patch( + "hmt_escrow.job.upload" + ) as upload_mock, patch.object( + Job, "_check_transfer_event" + ) as _check_transfer_event_mock: key = "abcdefg" hash_ = f"s3{key}" upload_mock.return_value = hash_, key + _check_transfer_event_mock.return_value = True # Bulk payout with option to store final results privately job.bulk_payout( @@ -397,6 +415,10 @@ def test_retrieving_encrypted_final_results(self): self.assertEqual(persisted_final_results, final_results) # Bulk payout with encryption OFF + job = Job(self.credentials, manifest) + self.assertEqual(job.launch(self.rep_oracle_pub_key), True) + self.assertEqual(job.setup(), True) + job.bulk_payout( payouts=payouts, results=final_results,