-
-
Notifications
You must be signed in to change notification settings - Fork 775
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Start tx validator v2, and fix bug in zkSync validations #7510
Merged
thelostone-mc
merged 3 commits into
gitcoinco:stable
from
ScopeLift:rewrite-tx-validator
Sep 24, 2020
Merged
Changes from 1 commit
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ | |
from bs4 import BeautifulSoup | ||
from dashboard.abi import erc20_abi | ||
from dashboard.utils import get_tx_status, get_web3 | ||
from economy.models import Token | ||
from hexbytes import HexBytes | ||
from web3 import HTTPProvider, Web3 | ||
from web3.exceptions import BadFunctionCallOutput | ||
|
@@ -50,6 +51,9 @@ class TransactionNotFound(Exception): | |
check_event_transfer = lambda contract_address, search, txid : w3.eth.filter({ "address": contract_address, "topics": [search, txid]}) | ||
get_decimals = lambda contract : int(contract.functions.decimals().call()) | ||
|
||
# BulkCheckout parameters | ||
bulk_checkout_address = "0x7d655c57f71464B6f83811C55D84009Cd9f5221C" # same address on mainnet and rinkeby | ||
bulk_checkout_abi = '[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":false,"internalType":"address","name":"dest","type":"address"},{"indexed":true,"internalType":"address","name":"donor","type":"address"}],"name":"DonationSent","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":true,"internalType":"address","name":"dest","type":"address"}],"name":"TokenWithdrawn","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"Unpaused","type":"event"},{"inputs":[{"components":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"address payable","name":"dest","type":"address"}],"internalType":"struct BulkCheckout.Donation[]","name":"_donations","type":"tuple[]"}],"name":"donate","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"paused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address payable","name":"_dest","type":"address"}],"name":"withdrawEther","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_tokenAddress","type":"address"},{"internalType":"address","name":"_dest","type":"address"}],"name":"withdrawToken","outputs":[],"stateMutability":"nonpayable","type":"function"}]' | ||
|
||
# scrapes etherscan to get the replaced tx | ||
def getReplacedTX(tx): | ||
|
@@ -102,6 +106,185 @@ def check_transaction_contract(transaction_tax): | |
return transaction_status(transaction, transaction_tax) | ||
|
||
|
||
def get_token(token_symbol, network): | ||
""" | ||
For a given token symbol and amount, returns the token's details. For ETH, we change the | ||
token address to 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE since that's the address | ||
BulkCheckout uses to represent ETH (default here is the zero address) | ||
""" | ||
token = Token.objects.filter(network=network, symbol=token_symbol, approved=True).first().to_dict | ||
if token_symbol == 'ETH': | ||
token['addr'] = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' | ||
return token | ||
|
||
def parse_token_amount(token_symbol, amount, network): | ||
""" | ||
For a given token symbol and amount, returns the integer version in "wei", i.e. the integer | ||
form based on the token's number of decimals | ||
""" | ||
token = get_token(token_symbol, network) | ||
decimals = token['decimals'] | ||
parsed_amount = int(amount * 10 ** decimals) | ||
return parsed_amount | ||
|
||
def check_for_replaced_tx(tx_hash, network): | ||
""" | ||
Get status of the provided transaction hash, and look for a replacement transaction hash. If a | ||
replacement exists, return the status and hash of the new transaction | ||
""" | ||
status, timestamp = get_tx_status(tx_hash, network, timezone.now()) | ||
if status in ['pending', 'dropped', 'unknown', '']: | ||
new_tx = getReplacedTX(tx_hash) | ||
if new_tx: | ||
tx_hash = new_tx | ||
status, timestamp = get_tx_status(tx_hash, network, timezone.now()) | ||
|
||
return tx_hash, status, timestamp | ||
|
||
def is_bulk_checkout_tx(receipt): | ||
""" | ||
Returns true if the to address of the recipient is the bulk checkout contract | ||
""" | ||
to_address = receipt['to'].lower() | ||
is_bulk_checkout = to_address == bulk_checkout_address.lower() | ||
return is_bulk_checkout | ||
|
||
def grants_transaction_validator_v2(contribution, w3): | ||
""" | ||
This function is used to validate contributions sent on L1 through the BulkCheckout contract. | ||
This contract can be found here: | ||
- On GitHub: https://github.com/gitcoinco/BulkTransactions/blob/master/contracts/BulkCheckout.sol | ||
- On mainnet: https://rinkeby.etherscan.io/address/0x7d655c57f71464b6f83811c55d84009cd9f5221c#code | ||
|
||
To facilitate testing on Rinkeby, we pass in a web3 instance instead of using the mainnet | ||
instance defined at the top of this file | ||
""" | ||
|
||
# Get bulk checkout contract instance | ||
bulk_checkout_contract = w3.eth.contract(address=bulk_checkout_address, abi=bulk_checkout_abi) | ||
|
||
# Get specific info about this contribution that we use later | ||
tx_hash = contribution.split_tx_id | ||
network = contribution.subscription.network | ||
|
||
# Response that calling function uses to set fields on Contribution | ||
response = { | ||
# Set passed to True if matching transfer is found for this contribution. The comment | ||
# field is used to provide details when false | ||
'validation': { | ||
'passed': False, | ||
'comment': 'Default' | ||
}, | ||
# Array of addresses where funds were intially sourced from. This is used to detect someone | ||
# funding many addresses from a single address. This functionality is currently not | ||
# implemented in grants_transaction_validator_v2 so for now we assume the originator is | ||
# msg.sender | ||
'originator': [ '' ], | ||
# Once tx_cleared is true, the validator is not run again for this contribution | ||
'tx_cleared': False, | ||
# True if the checkout transaction was mined | ||
'split_tx_confirmed': False | ||
} | ||
|
||
# Return if tx_hash is not valid | ||
if not tx_hash or len(tx_hash) != 66: | ||
# Set to true so this doesn't run again, since there's no transaction hash to check | ||
response['tx_cleared'] = True | ||
response['validation']['comment'] = 'Invalid transaction hash in split_tx_id' | ||
return response | ||
|
||
# Check for dropped and replaced txn | ||
tx_hash, status, timestamp = check_for_replaced_tx(tx_hash, network) | ||
|
||
# If transaction was successful, continue to validate it | ||
if status == 'success': | ||
# Transaction was successful so we know it cleared | ||
response['tx_cleared'] = True | ||
response['split_tx_confirmed'] = True | ||
|
||
# Get the receipt to parse parameters | ||
receipt = w3.eth.getTransactionReceipt(tx_hash) | ||
|
||
# Validator currently assumes msg.sender == originator | ||
response['originator'] = [ receipt['from'] ] | ||
|
||
# Return if recipient is not the BulkCheckout contract | ||
is_bulk_checkout = is_bulk_checkout_tx(receipt) | ||
if not is_bulk_checkout: | ||
to_address = receipt['to'] | ||
response['validation']['comment'] = f'This function only validates transactions through the BulkCheckout contract, but this transaction was sent to {to_address}' | ||
return response | ||
|
||
# Parse receipt logs to look for expected transfer info. We don't need to look at any other | ||
# receipt parameters because all contributions are emitted as an event | ||
receipt = w3.eth.getTransactionReceipt(tx_hash) | ||
parsed_logs = bulk_checkout_contract.events.DonationSent().processReceipt(receipt) | ||
|
||
# Return if no donation logs were found | ||
if (len(parsed_logs) == 0): | ||
response['validation']['comment'] = 'No DonationSent events found in this BulkCheckout transaction' | ||
return response | ||
|
||
# Parse out the transfer details we are looking to find in the event logs | ||
token_symbol = contribution.normalized_data['token_symbol'] | ||
expected_recipient = contribution.normalized_data['admin_address'].lower() | ||
expected_token = get_token(token_symbol, network)['addr'].lower() # we compare by token address | ||
expected_amount = parse_token_amount( | ||
token_symbol=token_symbol, | ||
amount=contribution.subscription.amount_per_period_minus_gas_price, | ||
network=network | ||
) | ||
transfer_tolerance = 0.05 # use a 5% tolerance when checking amounts to account for floating point error | ||
expected_amount_min = int(expected_amount * (1 - transfer_tolerance)) | ||
expected_amount_max = int(expected_amount * (1 + transfer_tolerance)) | ||
|
||
# Loop through each event to find one that matches | ||
for event in parsed_logs: | ||
is_correct_recipient = event['args']['dest'].lower() == expected_recipient | ||
is_correct_token = event['args']['token'].lower() == expected_token | ||
|
||
transfer_amount = event['args']['amount'] | ||
is_correct_amount = transfer_amount > expected_amount_min and transfer_amount < expected_amount_max | ||
|
||
if is_correct_recipient and is_correct_token and is_correct_amount: | ||
# We found the event log corresponding to the contribution parameters | ||
response['validation']['passed'] = True | ||
response['validation']['comment'] = 'BulkCheckout. Success' | ||
return response | ||
|
||
# Transaction was successful, but the expected contribution was not included in the transaction | ||
response['validation']['comment'] = 'DonationSent event with expected recipient, amount, and token was not found in transaction logs' | ||
return response | ||
|
||
# If we get here, none of the above failure conditions have been met, so we try to find | ||
# more information about why it failed | ||
if status == 'pending': | ||
response['validation']['comment'] = 'Transaction is still pending' | ||
return response | ||
|
||
try: | ||
# Get receipt and set originator to msg.sender | ||
receipt = w3.eth.getTransactionReceipt(tx_hash) | ||
response['originator'] = [ receipt['from'] ] | ||
|
||
if receipt.status == 0: | ||
# Transaction was minded, but | ||
response['tx_cleared'] = True | ||
response['split_tx_confirmed'] = True | ||
response['validation']['comment'] = 'Transaction failed. See Etherscan for more details' | ||
return response | ||
|
||
# If here, transaction was successful. This code block should never execute, but it means | ||
# the transaction was successful but for some reason not parsed above | ||
raise Exception('Unknown transaction validation flow 1') | ||
|
||
except w3.exceptions.TransactionNotFound: | ||
response['validation']['comment'] = 'Transaction receipt not found. Transaction may still be pending or was dropped' | ||
return response | ||
|
||
raise Exception('Unknown transaction validation flow 2') | ||
|
||
|
||
def grants_transaction_validator(contribution, w3): | ||
# To facilitate testing on Rinkeby, we pass in a web3 instance instead of using the mainnet | ||
# instance defined at the top of this file | ||
|
@@ -147,14 +330,17 @@ def grants_transaction_validator(contribution, w3): | |
transaction = w3.eth.getTransaction(transaction_hash) | ||
if transaction.value > 0.001: | ||
recipient_address = Web3.toChecksumAddress(contribution.subscription.grant.admin_address) | ||
print(1111) | ||
transfer = get_token_originators(recipient_address, '0x0', from_address=from_address, return_what='transfers', tx_id=tx, amounts=amounts) | ||
if not transfer: | ||
print(2222) | ||
transfer = get_token_originators(recipient_address, '0x0', from_address=from_address, return_what='transfers', tx_id=tx) | ||
if transfer: | ||
token_transfer = transfer | ||
maybeprint(148, round(time.time(),2)) | ||
if not token_originators: | ||
|
||
print(3333) | ||
token_originators = get_token_originators(from_address, '0x0', from_address=None, return_what='originators') | ||
|
||
maybeprint(150, round(time.time(),2)) | ||
|
@@ -170,11 +356,13 @@ def grants_transaction_validator(contribution, w3): | |
maybeprint(160, round(time.time(),2)) | ||
# get token transfers | ||
if not token_transfer: | ||
print(4444) | ||
transfers = get_token_originators(recipient_address, token_address, from_address=from_address, return_what='transfers', tx_id=tx, amounts=amounts) | ||
if transfers: | ||
token_transfer = transfers | ||
maybeprint(169, round(time.time(),2)) | ||
if not token_originators: | ||
print(5555) | ||
token_originators = get_token_originators(from_address, token_address, from_address=None, return_what='originators') | ||
maybeprint(170, round(time.time(),2)) | ||
|
||
|
@@ -294,19 +482,26 @@ def get_token_originators(to_address, token, from_address='', return_what='trans | |
endpoint = 'token-transfers' if token != '0x0' else 'ether-transfers' | ||
url = f'https://api.aleth.io/v1/{endpoint}?filter[to]=' + address + '&filter[token]=' + token + '&page%5Blimit%5D=100' | ||
if token == '0x0': | ||
print('qqqq') | ||
url = f'https://api.aleth.io/v1/{endpoint}?filter[account]=' + address + '&page%5Blimit%5D=100' | ||
if from_address: | ||
print('wwww') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need the prints? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops, yea those were leftover from my expert debugging techniques. Removed in f61690e |
||
url += '&filter[from]=' + from_address | ||
|
||
# OLD: THIS REQUEST THROWS WITH A 500 INTERNAL SERVER ERROR | ||
transfers = requests.get( | ||
url, | ||
headers=headers | ||
).json() | ||
|
||
# NEW: PARSE EVENT LOGS TO SEE WHAT'S GOING ON | ||
|
||
if transfers.get('message') == 'API rate limit exceeded. Please upgrade your account.': | ||
raise Exception("RATE LIMIT EXCEEDED") | ||
# TODO - pull more than one page in case there are many transfers. | ||
|
||
if return_what == 'transfers': | ||
print(1) | ||
for transfer in transfers.get('data', {}): | ||
this_is_the_one = tx_id and tx_id.lower() in str(transfer).lower() | ||
_decimals = transfer.get('attributes', {}).get('decimals', 18) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^ do we wanna remove all the print stmt :P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, yea those were leftover from my expert debugging techniques. Removed in f61690e