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

Commit 27c006e

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

File tree

8 files changed

+131
-14
lines changed

8 files changed

+131
-14
lines changed

Pipfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ hmt-basemodels = "==0.1.18"
1212
py-solc-x = "*"
1313
sphinx = "*"
1414
web3 = "==5.24.0"
15+
pysha3 = "==1.0.2"
1516

1617
[requires]
1718
python_version = "3.10"

Pipfile.lock

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

hmt_escrow/eth_bridge.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from web3.providers.auto import load_provider_from_uri
1313
from web3.providers.eth_tester import EthereumTesterProvider
1414
from web3.types import TxReceipt
15+
import sha3
1516

1617
from hmt_escrow.kvstore_abi import abi as kvstore_abi
1718

@@ -437,3 +438,34 @@ def set_pub_key_at_addr(
437438
}
438439

439440
return handle_transaction(txn_func, *func_args, **txn_info)
441+
442+
443+
def get_entity_topic(contract: str, eventname: str) -> str:
444+
"""
445+
Args:
446+
contract (str): contract name ex.: EscrowFactory.sol:EscrowFactory.
447+
448+
eventname (str): event name to find in abi.
449+
450+
Returns
451+
str: returns keccak_256 hash of event name with input parameters.
452+
"""
453+
contract_interface = get_contract_interface(
454+
"{}/{}".format(CONTRACT_FOLDER, contract)
455+
)
456+
s = ""
457+
458+
for entity in contract_interface["abi"]:
459+
eventName = entity.get("name")
460+
if eventName == eventname:
461+
s += eventName + "("
462+
inputs = entity.get("inputs", [])
463+
inputTypes = []
464+
for input in inputs:
465+
inputTypes.append(input.get("internalType"))
466+
s += ",".join(inputTypes) + ")"
467+
468+
k = sha3.keccak_256()
469+
k.update(s.encode("utf-8"))
470+
471+
return k.hexdigest()

hmt_escrow/job.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from hmt_escrow import utils
1616
from hmt_escrow.eth_bridge import (
1717
get_hmtoken,
18+
get_entity_topic,
1819
get_escrow,
1920
get_factory,
2021
deploy_factory,
@@ -26,6 +27,7 @@
2627
from hmt_escrow.storage import download, upload, get_public_bucket_url, get_key_from_url
2728

2829
GAS_LIMIT = int(os.getenv("GAS_LIMIT", 4712388))
30+
BULK_TRANSFER_EVENT = get_entity_topic("HMToken.sol:HMToken", "BulkTransfer")
2931

3032
# Explicit env variable that will use s3 for storing results.
3133

@@ -617,6 +619,7 @@ def bulk_payout(
617619
bool: returns True if paying to ethereum addresses and oracles succeeds.
618620
619621
"""
622+
bulk_paid = False
620623
txn_event = "Bulk payout"
621624
txn_func = self.job_contract.functions.bulkPayOut
622625
txn_info = {
@@ -646,23 +649,35 @@ def bulk_payout(
646649
func_args = [eth_addrs, hmt_amounts, url, hash_, 1]
647650

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

652660
except Exception as e:
653661
LOG.warn(
654662
f"{txn_event} failed with main credentials: {self.gas_payer}, {self.gas_payer_priv} due to {e}. Using secondary ones..."
655663
)
656664

665+
if bulk_paid:
666+
return bulk_paid
667+
668+
LOG.warn(
669+
f"{txn_event} failed with main credentials: {self.gas_payer}, {self.gas_payer_priv}. Using secondary ones..."
670+
)
671+
657672
raffle_txn_res = self._raffle_txn(
658673
self.multi_credentials, txn_func, func_args, txn_event
659674
)
660-
bulk_paid = raffle_txn_res["txn_succeeded"]
675+
bulk_paid = self._check_transfer_event(raffle_txn_res["tx_receipt"])
661676

662677
if not bulk_paid:
663678
LOG.warning(f"{txn_event} failed with all credentials.")
664679

665-
return bulk_paid is True
680+
return bulk_paid
666681

667682
def abort(self) -> bool:
668683
"""Kills the contract and returns the HMT back to the gas payer.
@@ -1508,3 +1523,22 @@ def _raffle_txn(self, multi_creds, txn_func, txn_args, txn_event) -> RaffleTxn:
15081523
)
15091524

15101525
return {"txn_succeeded": txn_succeeded, "tx_receipt": tx_receipt}
1526+
1527+
def _check_transfer_event(self, tx_receipt: Optional[TxReceipt]) -> bool:
1528+
"""
1529+
Check if transaction receipt has bulkTransfer event, to make sure that transaction was successful.
1530+
1531+
Args:
1532+
tx_receipt (Optional[TxReceipt]): a dict with transaction receipt.
1533+
1534+
Returns:
1535+
bool: returns True if transaction has bulkTransfer event, otherwise returns False.
1536+
"""
1537+
if not tx_receipt:
1538+
return False
1539+
1540+
for log in tx_receipt.get("logs", {}):
1541+
for topic in log["topics"]:
1542+
if BULK_TRANSFER_EVENT in topic.hex():
1543+
return True
1544+
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: 2 additions & 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",
@@ -18,6 +18,7 @@
1818
"boto3",
1919
"cryptography",
2020
"hmt-basemodels>=0.1.18",
21+
"pysha3==1.0.2",
2122
"web3==5.24.0",
2223
],
2324
)

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)