Skip to content
This repository was archived by the owner on Feb 16, 2023. It is now read-only.

Commit eccf82a

Browse files
committed
Read bulk_payout logs to avoid double payouts
1 parent 1886fe6 commit eccf82a

6 files changed

Lines changed: 109 additions & 16 deletions

File tree

hmt_escrow/eth_bridge.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,19 @@ def get_contract_interface(contract_entrypoint):
200200
return contract_interface
201201

202202

203+
def get_hmtoken_interface():
204+
"""Retrieve the HMToken interface.
205+
206+
Returns:
207+
Contract interface: returns the HMToken interface solidity contract.
208+
209+
"""
210+
211+
return get_contract_interface(
212+
"{}/HMTokenInterface.sol:HMTokenInterface".format(CONTRACT_FOLDER)
213+
)
214+
215+
203216
def get_hmtoken(hmtoken_addr=HMTOKEN_ADDR, hmt_server_addr: str = None) -> Contract:
204217
"""Retrieve the HMToken contract from a given address.
205218
@@ -214,9 +227,7 @@ def get_hmtoken(hmtoken_addr=HMTOKEN_ADDR, hmt_server_addr: str = None) -> Contr
214227
215228
"""
216229
w3 = get_w3(hmt_server_addr)
217-
contract_interface = get_contract_interface(
218-
"{}/HMTokenInterface.sol:HMTokenInterface".format(CONTRACT_FOLDER)
219-
)
230+
contract_interface = get_hmtoken_interface()
220231
contract = w3.eth.contract(address=hmtoken_addr, abi=contract_interface["abi"])
221232
return contract
222233

@@ -437,3 +448,28 @@ def set_pub_key_at_addr(
437448
}
438449

439450
return handle_transaction(txn_func, *func_args, **txn_info)
451+
452+
453+
def get_entity_topic(contract_interface: Dict, name: str) -> str:
454+
"""
455+
Args:
456+
contract_interface (Dict): contract inteface.
457+
458+
name (str): event name to find in abi.
459+
460+
Returns
461+
str: returns keccak_256 hash of event name with input parameters.
462+
"""
463+
s = ""
464+
465+
for entity in contract_interface["abi"]:
466+
event_name = entity.get("name")
467+
if event_name == name:
468+
s += event_name + "("
469+
inputs = entity.get("inputs", [])
470+
input_types = []
471+
for input in inputs:
472+
input_types.append(input.get("internalType"))
473+
s += ",".join(input_types) + ")"
474+
475+
return Web3.keccak(text=s)

hmt_escrow/job.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from hmt_escrow import utils
1616
from hmt_escrow.eth_bridge import (
1717
get_hmtoken,
18+
get_hmtoken_interface,
19+
get_entity_topic,
1820
get_escrow,
1921
get_factory,
2022
deploy_factory,
@@ -26,6 +28,7 @@
2628
from hmt_escrow.storage import download, upload, get_public_bucket_url, get_key_from_url
2729

2830
GAS_LIMIT = int(os.getenv("GAS_LIMIT", 4712388))
31+
TRANSFER_EVENT = get_entity_topic(get_hmtoken_interface(), "Transfer")
2932

3033
# Explicit env variable that will use s3 for storing results.
3134

@@ -617,6 +620,7 @@ def bulk_payout(
617620
bool: returns True if paying to ethereum addresses and oracles succeeds.
618621
619622
"""
623+
bulk_paid = False
620624
txn_event = "Bulk payout"
621625
txn_func = self.job_contract.functions.bulkPayOut
622626
txn_info = {
@@ -646,23 +650,35 @@ def bulk_payout(
646650
func_args = [eth_addrs, hmt_amounts, url, hash_, 1]
647651

648652
try:
649-
handle_transaction_with_retry(txn_func, self.retry, *func_args, **txn_info)
650-
return self._bulk_paid() is True
653+
tx_receipt = handle_transaction_with_retry(
654+
txn_func,
655+
self.retry,
656+
*func_args,
657+
**txn_info,
658+
)
659+
bulk_paid = self._check_transfer_event(tx_receipt)
651660

652661
except Exception as e:
653662
LOG.warn(
654663
f"{txn_event} failed with main credentials: {self.gas_payer}, {self.gas_payer_priv} due to {e}. Using secondary ones..."
655664
)
656665

666+
if bulk_paid:
667+
return bulk_paid
668+
669+
LOG.warn(
670+
f"{txn_event} failed with main credentials: {self.gas_payer}, {self.gas_payer_priv}. Using secondary ones..."
671+
)
672+
657673
raffle_txn_res = self._raffle_txn(
658674
self.multi_credentials, txn_func, func_args, txn_event
659675
)
660-
bulk_paid = raffle_txn_res["txn_succeeded"]
676+
bulk_paid = self._check_transfer_event(raffle_txn_res["tx_receipt"])
661677

662678
if not bulk_paid:
663679
LOG.warning(f"{txn_event} failed with all credentials.")
664680

665-
return bulk_paid is True
681+
return bulk_paid
666682

667683
def abort(self) -> bool:
668684
"""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:
15081524
)
15091525

15101526
return {"txn_succeeded": txn_succeeded, "tx_receipt": tx_receipt}
1527+
1528+
def _check_transfer_event(self, tx_receipt: Optional[TxReceipt]) -> bool:
1529+
"""
1530+
Check if transaction receipt has bulkTransfer event, to make sure that transaction was successful.
1531+
1532+
Args:
1533+
tx_receipt (Optional[TxReceipt]): a dict with transaction receipt.
1534+
1535+
Returns:
1536+
bool: returns True if transaction has bulkTransfer event, otherwise returns False.
1537+
"""
1538+
if not tx_receipt:
1539+
return False
1540+
1541+
for log in tx_receipt.get("logs", {}):
1542+
for topic in log["topics"]:
1543+
if TRANSFER_EVENT == topic:
1544+
return True
1545+
return False

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hmt-escrow",
3-
"version": "0.14.6",
3+
"version": "0.14.7",
44
"description": "Launch escrow contracts to the HUMAN network",
55
"main": "truffle.js",
66
"directories": {

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
setuptools.setup(
44
name="hmt-escrow",
5-
version="0.14.6",
5+
version="0.14.7",
66
author="HUMAN Protocol",
77
description="A python library to launch escrow contracts to the HUMAN network.",
88
url="https://github.com/humanprotocol/hmt-escrow",

test/hmt_escrow/test_job.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,8 @@ def test_job_bulk_payout(self):
225225
]
226226
self.assertTrue(self.job.bulk_payout(payouts, {}, self.rep_oracle_pub_key))
227227

228-
def test_job_bulk_payout_with_encryption_option(self):
229-
"""Tests whether final results must be persisted in storage encrypted or plain."""
228+
def test_job_bulk_payout_with_false_encryption_option(self):
229+
"""Test that final results are stored encrypted"""
230230
job = Job(self.credentials, manifest)
231231
self.assertEqual(job.launch(self.rep_oracle_pub_key), True)
232232
self.assertEqual(job.setup(), True)
@@ -253,10 +253,24 @@ def test_job_bulk_payout_with_encryption_option(self):
253253
encrypt_data=False,
254254
use_public_bucket=False,
255255
)
256-
mock_upload.reset_mock()
257256

258-
# Testing option as: encrypt final results: encrypt_final_results=True
257+
def test_job_bulk_payout_with_true_encryption_option(self):
258+
"""Test that final results are stored uncrypted"""
259+
job = Job(self.credentials, manifest)
260+
self.assertEqual(job.launch(self.rep_oracle_pub_key), True)
261+
self.assertEqual(job.setup(), True)
262+
263+
payouts = [("0x852023fbb19050B8291a335E5A83Ac9701E7B4E6", Decimal("100.0"))]
264+
265+
final_results = {"results": 0}
266+
267+
mock_upload = MagicMock(return_value=("hash", "url"))
268+
269+
# Testing option as: encrypt final results: encrypt_final_results=True
270+
with patch("hmt_escrow.job.upload") as mock_upload:
259271
# Bulk payout with final results as plain (not encrypted)
272+
mock_upload.return_value = ("hash", "url")
273+
260274
job.bulk_payout(
261275
payouts=payouts,
262276
results=final_results,
@@ -325,15 +339,19 @@ def test_job_bulk_payout_with_full_qualified_url(self):
325339
self.assertEqual(job.setup(), True)
326340

327341
payouts = [("0x852023fbb19050B8291a335E5A83Ac9701E7B4E6", Decimal("100.0"))]
328-
329342
final_results = {"results": 0}
330343

331344
with patch(
332345
"hmt_escrow.job.handle_transaction_with_retry"
333-
) as transaction_retry_mock, patch("hmt_escrow.job.upload") as upload_mock:
346+
) as transaction_retry_mock, patch(
347+
"hmt_escrow.job.upload"
348+
) as upload_mock, patch.object(
349+
Job, "_check_transfer_event"
350+
) as _check_transfer_event_mock:
334351
key = "abcdefg"
335352
hash_ = f"s3{key}"
336353
upload_mock.return_value = hash_, key
354+
_check_transfer_event_mock.return_value = True
337355

338356
# Bulk payout with option to store final results privately
339357
job.bulk_payout(
@@ -397,6 +415,10 @@ def test_retrieving_encrypted_final_results(self):
397415
self.assertEqual(persisted_final_results, final_results)
398416

399417
# Bulk payout with encryption OFF
418+
job = Job(self.credentials, manifest)
419+
self.assertEqual(job.launch(self.rep_oracle_pub_key), True)
420+
self.assertEqual(job.setup(), True)
421+
400422
job.bulk_payout(
401423
payouts=payouts,
402424
results=final_results,

0 commit comments

Comments
 (0)