Skip to content

Commit

Permalink
Merge pull request #44 from gnosischain/feature/add-timer
Browse files Browse the repository at this point in the history
Add waiting period to funds claiming
  • Loading branch information
giacomognosis authored Jun 3, 2024
2 parents db980a1 + 962ce58 commit 2a7c218
Show file tree
Hide file tree
Showing 12 changed files with 132 additions and 53 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,17 @@ flask -A api create_enabled_token GNO 10200 0x19C653Da7c37c66208fbfbE8908A5051B5
flask -A api create_enabled_token xDAI 10200 0x0000000000000000000000000000000000000000 0.01 native
```

Once enabled, the token wil appear in the list of enabled tokens on the endpoint `api/v1/info`.
Once enabled, the token will appear in the list of enabled tokens on the endpoint `api/v1/info`.

#### Change maximum daily amounts per user

If you want to change the amount you are giving out for a specific token, make sure you have sqlite
installed on the server, e.g. apk update && apk add sqlite.

Enter the database: `sqlite path/to/database`

Search for the token to update: `select chain_id, max_amount_day from tokens where name = 'xDAI'`
Update amount: `update tokens set max_amount_day = 0.00015 where chain_id = 100;`

## ReactJS Frontend

Expand Down
3 changes: 2 additions & 1 deletion api/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ def info():
chainName=current_app.config['FAUCET_CHAIN_NAME'],
faucetAddress=current_app.config['FAUCET_ADDRESS'],
csrfToken=csrf_item.token,
csrfRequestId=csrf_item.request_id
csrfRequestId=csrf_item.request_id,
csrfTimestamp=csrf_item.timestamp
), 200


Expand Down
30 changes: 22 additions & 8 deletions api/api/services/csrf.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
# from api.settings import CSRF_TIMESTAMP_MAX_SECONDS

import random
from datetime import datetime

from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA

# Waiting period: the minimum time interval between UI asks for the CSFR token
# and the time it asks for funds.
CSRF_TIMESTAMP_MIN_SECONDS = 15


class CSRFTokenItem:
def __init__(self, request_id, token):
def __init__(self, request_id, token, timestamp):
self.request_id = request_id
self.token = token
self.timestamp = timestamp


class CSRFToken:
Expand All @@ -17,23 +24,30 @@ def __init__(self, privkey, salt):
self._pubkey = self._privkey.publickey()
self._salt = salt

def generate_token(self):
def generate_token(self, timestamp=None):
request_id = '%d' % random.randint(0, 1000)
data_to_encrypt = '%s%s' % (request_id, self._salt)
if not timestamp:
timestamp = datetime.now().timestamp()
data_to_encrypt = '{"requestId":"%s","salt":"%s","timestamp":"%f"}' % (request_id, self._salt, timestamp)

cipher_rsa = PKCS1_OAEP.new(self._pubkey)
token = cipher_rsa.encrypt(data_to_encrypt.encode())

return CSRFTokenItem(request_id, token.hex())
return CSRFTokenItem(request_id, token.hex(), timestamp)

def validate_token(self, request_id, token):
def validate_token(self, request_id, token, timestamp):
try:
cipher_rsa = PKCS1_OAEP.new(self._privkey)
decrypted_text = cipher_rsa.decrypt(bytes.fromhex(token)).decode()

expected_text = '%s%s' % (request_id, self._salt)
expected_text = '{"requestId":"%s","salt":"%s","timestamp":"%f"}' % (request_id, self._salt, timestamp)
if decrypted_text == expected_text:
return True
# Check that timestamp is OK, the diff between now() and creation time in seconds
# must be greater than min. waiting period.
# Waiting period: the minimum time interval between UI asks for the CSFR token and the time it asks for funds.
seconds_diff = (datetime.now()-datetime.fromtimestamp(timestamp)).total_seconds()
if seconds_diff > CSRF_TIMESTAMP_MIN_SECONDS:
return True
return False
return False
except Exception:
return False
Expand Down
7 changes: 6 additions & 1 deletion api/api/services/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,12 @@ def csrf_validation(self):
self.errors.append('Bad request')
self.http_return_code = 400

csrf_valid = self.csrf.validate_token(request_id, token)
timestamp = self.request_data.get('timestamp', None)
if not timestamp:
self.errors.append('Bad request')
self.http_return_code = 400

csrf_valid = self.csrf.validate_token(request_id, token, timestamp)
if not csrf_valid:
self.errors.append('Bad request')
self.http_return_code = 400
Expand Down
8 changes: 4 additions & 4 deletions api/scripts/local_test_run.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FLASK_APP=api FAUCET_DATABASE_URI=sqlite:///test.db python3 -m flask db upgrade
FLASK_APP=api FAUCET_DATABASE_URI=sqlite:///test.db python3 -m flask create_enabled_token xDAI 10200 0x0000000000000000000000000000000000000000 10 native
FLASK_APP=api FAUCET_DATABASE_URI=sqlite:///test.db python3 -m flask create_access_keys
FLASK_APP=api FAUCET_DATABASE_URI=sqlite:///$(pwd)/test.db python3 -m flask db upgrade
FLASK_APP=api FAUCET_DATABASE_URI=sqlite:///$(pwd)/test.db python3 -m flask create_enabled_token xDAI 10200 0x0000000000000000000000000000000000000000 0.0001 native
FLASK_APP=api FAUCET_DATABASE_URI=sqlite:///$(pwd)/test.db python3 -m flask create_access_keys
# Take note of the access keys
# Run API on port 3000
FLASK_APP=api FAUCET_DATABASE_URI=sqlite:///test.db python3 -m flask run -p 3000
FLASK_APP=api FAUCET_DATABASE_URI=sqlite:///$(pwd)/test.db python3 -m flask run -p 8000
Binary file added api/test.db
Binary file not shown.
13 changes: 9 additions & 4 deletions api/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
from unittest import TestCase, TestResult, mock
from unittest import TestCase, mock
from datetime import datetime

from api.services import CSRF, Strategy
from api.services.database import Token, db
Expand All @@ -13,6 +14,7 @@


class BaseTest(TestCase):
valid_csrf_timestamp = datetime(2020, 1, 18, 9, 30, 0).timestamp()

def mock_claim_native(self, *args):
tx_hash = '0x0' + '%d' % self.native_tx_counter * 63
Expand Down Expand Up @@ -73,7 +75,8 @@ def setUp(self):

self.csrf = CSRF.instance
# use same token for the whole test
self.csrf_token = self.csrf.generate_token()
# use a timestamp that would be actually validated by the CSRF class.
self.csrf_token = self.csrf.generate_token(timestamp=self.valid_csrf_timestamp)

def tearDown(self):
'''
Expand Down Expand Up @@ -105,7 +108,8 @@ def setUp(self):

self.csrf = CSRF.instance
# use same token for the whole test
self.csrf_token = self.csrf.generate_token()
# use a timestamp that would be actually validated by the CSRF class.
self.csrf_token = self.csrf.generate_token(timestamp=self.valid_csrf_timestamp)


class RateLimitIPorAddressBaseTest(BaseTest):
Expand All @@ -128,4 +132,5 @@ def setUp(self):

self.csrf = CSRF.instance
# use same token for the whole test
self.csrf_token = self.csrf.generate_token()
# use a timestamp that would be actually validated by the CSRF class.
self.csrf_token = self.csrf.generate_token(timestamp=self.valid_csrf_timestamp)
36 changes: 24 additions & 12 deletions api/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ def test_ask_route_parameters(self):
'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY,
'recipient': ZERO_ADDRESS,
'tokenAddress': NATIVE_TOKEN_ADDRESS,
'requestId': self.csrf_token.request_id
'requestId': self.csrf_token.request_id,
'timestamp': self.csrf_token.timestamp
}, headers={
'X-CSRFToken': self.csrf_token.token
})
Expand All @@ -48,7 +49,8 @@ def test_ask_route_parameters(self):
'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY + 1,
'recipient': ZERO_ADDRESS,
'tokenAddress': NATIVE_TOKEN_ADDRESS,
'requestId': self.csrf_token.request_id
'requestId': self.csrf_token.request_id,
'timestamp': self.csrf_token.timestamp
}, headers={
'X-CSRFToken': self.csrf_token.token
})
Expand All @@ -60,7 +62,8 @@ def test_ask_route_parameters(self):
'chainId': FAUCET_CHAIN_ID,
'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY + 1,
'tokenAddress': NATIVE_TOKEN_ADDRESS,
'requestId': self.csrf_token.request_id
'requestId': self.csrf_token.request_id,
'timestamp': self.csrf_token.timestamp
}, headers={
'X-CSRFToken': self.csrf_token.token
})
Expand All @@ -73,7 +76,8 @@ def test_ask_route_parameters(self):
'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY + 1,
'recipient': 'not an address',
'tokenAddress': NATIVE_TOKEN_ADDRESS,
'requestId': self.csrf_token.request_id
'requestId': self.csrf_token.request_id,
'timestamp': self.csrf_token.timestamp
}, headers={
'X-CSRFToken': self.csrf_token.token
})
Expand All @@ -85,7 +89,8 @@ def test_ask_route_parameters(self):
'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY,
'recipient': '0x00000123',
'tokenAddress': ERC20_TOKEN_ADDRESS,
'requestId': self.csrf_token.request_id
'requestId': self.csrf_token.request_id,
'timestamp': self.csrf_token.timestamp
}, headers={
'X-CSRFToken': self.csrf_token.token
})
Expand All @@ -97,7 +102,8 @@ def test_ask_route_parameters(self):
'chainId': FAUCET_CHAIN_ID,
'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY + 1,
'recipient': ZERO_ADDRESS,
'requestId': self.csrf_token.request_id
'requestId': self.csrf_token.request_id,
'timestamp': self.csrf_token.timestamp
}, headers={
'X-CSRFToken': self.csrf_token.token
})
Expand All @@ -110,7 +116,8 @@ def test_ask_route_parameters(self):
'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY + 1,
'recipient': ZERO_ADDRESS,
'tokenAddress': 'non existing token address',
'requestId': self.csrf_token.request_id
'requestId': self.csrf_token.request_id,
'timestamp': self.csrf_token.timestamp
}, headers={
'X-CSRFToken': self.csrf_token.token
})
Expand All @@ -123,7 +130,8 @@ def test_ask_route_native_transaction(self):
'amount': DEFAULT_NATIVE_MAX_AMOUNT_PER_DAY,
'recipient': ZERO_ADDRESS,
'tokenAddress': NATIVE_TOKEN_ADDRESS,
'requestId': self.csrf_token.request_id
'requestId': self.csrf_token.request_id,
'timestamp': self.csrf_token.timestamp
}, headers={
'X-CSRFToken': self.csrf_token.token
})
Expand All @@ -140,7 +148,8 @@ def test_ask_route_token_transaction(self):
'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY,
'recipient': ZERO_ADDRESS,
'tokenAddress': '0x' + '1234' * 10,
'requestId': self.csrf_token.request_id
'requestId': self.csrf_token.request_id,
'timestamp': self.csrf_token.timestamp
}, headers={
'X-CSRFToken': self.csrf_token.token
})
Expand All @@ -152,7 +161,8 @@ def test_ask_route_token_transaction(self):
'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY,
'recipient': ZERO_ADDRESS,
'tokenAddress': ERC20_TOKEN_ADDRESS,
'requestId': self.csrf_token.request_id
'requestId': self.csrf_token.request_id,
'timestamp': self.csrf_token.timestamp
}, headers={
'X-CSRFToken': self.csrf_token.token
})
Expand All @@ -168,7 +178,8 @@ def test_ask_route_blocked_users(self):
'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY,
'recipient': ZERO_ADDRESS,
'tokenAddress': ERC20_TOKEN_ADDRESS,
'requestId': self.csrf_token.request_id
'requestId': self.csrf_token.request_id,
'timestamp': self.csrf_token.timestamp
}, headers={
'X-CSRFToken': self.csrf_token.token
})
Expand All @@ -184,7 +195,8 @@ def test_ask_route_blocked_users(self):
'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY,
'recipient': ZERO_ADDRESS,
'tokenAddress': ERC20_TOKEN_ADDRESS,
'requestId': self.csrf_token.request_id
'requestId': self.csrf_token.request_id,
'timestamp': self.csrf_token.timestamp
}, headers={
'X-CSRFToken': self.csrf_token.token
})
Expand Down
15 changes: 10 additions & 5 deletions api/tests/test_api_claim_rate_limit.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ def test_ask_route_limit_by_ip(self):
'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY,
'recipient': ZERO_ADDRESS,
'tokenAddress': ERC20_TOKEN_ADDRESS,
'requestId': self.csrf_token.request_id
'requestId': self.csrf_token.request_id,
'timestamp': self.valid_csrf_timestamp
}, headers={
'X-CSRFToken': self.csrf_token.token
})
Expand All @@ -33,7 +34,8 @@ def test_ask_route_limit_by_ip(self):
'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY,
'recipient': ZERO_ADDRESS,
'tokenAddress': ERC20_TOKEN_ADDRESS,
'requestId': self.csrf_token.request_id
'requestId': self.csrf_token.request_id,
'timestamp': self.valid_csrf_timestamp
}, headers={
'X-CSRFToken': self.csrf_token.token
})
Expand All @@ -49,7 +51,8 @@ def test_ask_route_limit_by_ip_or_address(self):
'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY,
'recipient': ZERO_ADDRESS,
'tokenAddress': ERC20_TOKEN_ADDRESS,
'requestId': self.csrf_token.request_id
'requestId': self.csrf_token.request_id,
'timestamp': self.valid_csrf_timestamp
}, headers={
'X-CSRFToken': self.csrf_token.token
})
Expand All @@ -66,7 +69,8 @@ def test_ask_route_limit_by_ip_or_address(self):
'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY,
'recipient': ZERO_ADDRESS,
'tokenAddress': ERC20_TOKEN_ADDRESS,
'requestId': self.csrf_token.request_id
'requestId': self.csrf_token.request_id,
'timestamp': self.valid_csrf_timestamp
}, headers={
'X-CSRFToken': self.csrf_token.token
})
Expand All @@ -84,7 +88,8 @@ def test_ask_route_limit_by_ip_or_address(self):
'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY,
'recipient': ZERO_ADDRESS,
'tokenAddress': ERC20_TOKEN_ADDRESS,
'requestId': self.csrf_token.request_id
'requestId': self.csrf_token.request_id,
'timestamp': self.valid_csrf_timestamp
}, headers={
'X-CSRFToken': self.csrf_token.token
})
Expand Down
21 changes: 16 additions & 5 deletions api/tests/test_csrf.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
from .conftest import BaseTest

from datetime import datetime


class TestCSRF(BaseTest):

def test_values(self):
token_obj = self.csrf.generate_token()
timestamp = datetime(2020, 1, 18, 9, 30, 0).timestamp()
token_obj = self.csrf.generate_token(timestamp=timestamp)
self.assertTrue(
self.csrf.validate_token(token_obj.request_id, token_obj.token)
self.csrf.validate_token(token_obj.request_id, token_obj.token, token_obj.timestamp)
)
self.assertFalse(
self.csrf.validate_token('myfakeid', token_obj.token, token_obj.timestamp)
)
self.assertFalse(
self.csrf.validate_token('myfakeid', token_obj.token)
self.csrf.validate_token('myfakeid', 'myfaketoken', token_obj.timestamp)
)
self.assertFalse(
self.csrf.validate_token('myfakeid', 'myfaketoken')
self.csrf.validate_token(token_obj.request_id, 'myfaketoken', token_obj.timestamp)
)
# test with timestamp for which diff between now() and creation time in seconds
# is lower than min. waiting period.
# Validation must return False since time interval is lower than mimimum waiting period.
timestamp = datetime.now().timestamp()
token_obj = self.csrf.generate_token(timestamp=timestamp)
self.assertFalse(
self.csrf.validate_token(token_obj.request_id, 'myfaketoken')
self.csrf.validate_token(token_obj.request_id, token_obj.token, token_obj.timestamp)
)
Loading

0 comments on commit 2a7c218

Please sign in to comment.