Skip to content

Commit

Permalink
FEAT: Epoch 0 GLM claiming
Browse files Browse the repository at this point in the history
  • Loading branch information
aziolek committed Aug 3, 2023
1 parent 19ec6d2 commit 07f66d0
Show file tree
Hide file tree
Showing 66 changed files with 1,911 additions and 909 deletions.
5 changes: 5 additions & 0 deletions backend/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ SCHEDULER_ENABLED=
SCHEDULER_API_ENABLED=
TESTNET_MULTISIG_PRIVATE_KEY=
VAULT_CONFIRM_WITHDRAWALS_ENABLED=

GLM_CLAIM_ENABLED=
GLM_SENDER_ADDRESS=
GLM_SENDER_PRIVATE_KEY=
GLM_SENDER_NONCE=
8 changes: 7 additions & 1 deletion backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ poetry install --no-interaction --no-ansi -v --with prod --without dev
poetry shell
```

Run the following commands to create your app's
Run the following commands to create your a
pp's
database tables and perform the initial migration

```bash
Expand Down Expand Up @@ -96,3 +97,8 @@ http://localhost:5000/docs/chain-info
- `TESTNET_MULTISIG_PRIVATE_KEY` - Multisig private key, which is allowed to send transactions to `Vault.sol`. Needed for automatic withdrawals confirmations for test purposes.
- `VAULT_CONFIRM_WITHDRAWALS_ENABLED` - Confirming withdrawals requires sending a merkle root to `Vault.sol`. For test purposes you can enable sending this value automatically by setting this var to `True`. In order for it to work scheduler must be enabled as well


- `GLM_CLAIM_ENABLED` - Set it to `True` if you want to enable GLM claiming feature. It requires also to enable scheduler.
- `GLM_SENDER_ADDRESS` - Address, from which GLMs will be sent.
- `GLM_SENDER_PRIVATE_KEY` - Private key corresponding to `GLM_SENDER_ADDRESS`
- `GLM_SENDER_NONCE` - Current nonce of the `GLM_SENDER_ADDRESS`
19 changes: 19 additions & 0 deletions backend/app/contracts/erc20.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@
"stateMutability": "view",
"type": "function",
},
{
"inputs": [
{"internalType": "address", "name": "to", "type": "address"},
{"internalType": "uint256", "name": "amount", "type": "uint256"},
],
"name": "transfer",
"outputs": [{"internalType": "bool", "name": "", "type": "bool"}],
"stateMutability": "nonpayable",
"type": "function",
},
]


Expand All @@ -32,6 +42,15 @@ def balance_of(self, address):
checksum_address = to_checksum_address(address)
return self.contract.functions.balanceOf(checksum_address).call()

def transfer(self, to_address, nonce):
transaction = self.contract.functions.transfer(
to_address, config.GLM_WITHDRAWAL_AMOUNT
).build_transaction({"from": config.GLM_SENDER_ADDRESS, "nonce": nonce})
signed_tx = w3.eth.account.sign_transaction(
transaction, config.GLM_SENDER_PRIVATE_KEY
)
return w3.eth.send_raw_transaction(signed_tx.rawTransaction)


glm = ERC20(config.GLM_CONTRACT_ADDRESS)
gnt = ERC20(config.GNT_CONTRACT_ADDRESS)
33 changes: 33 additions & 0 deletions backend/app/controllers/glm_claim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from flask import current_app as app

from app import database
from app.core import claims as claims_core
from app.crypto.eip712 import recover_address, build_claim_glm_eip712_data
from app.database.models import EpochZeroClaim
from app.exceptions import GlmClaimed, NotEligibleToClaimGLM
from app.extensions import db


def claim(signature: str):
eip712_data = build_claim_glm_eip712_data()
user_address = recover_address(eip712_data, signature)
nonce = claims_core.get_next_claim_nonce()
app.logger.info(f"User: {user_address} is claiming GLMs with a nonce: {nonce}")

user_claim = check(user_address)

user_claim.claimed = True
user_claim.claim_nonce = nonce
db.session.commit()


def check(address: str) -> EpochZeroClaim:
user_claim = database.claims.get_by_address(address)

if user_claim is None:
raise NotEligibleToClaimGLM(address)

if user_claim.claimed:
raise GlmClaimed(address)

return user_claim
7 changes: 7 additions & 0 deletions backend/app/core/claims.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from app import database
from app.settings import config


def get_next_claim_nonce() -> int:
last_nonce = database.claims.get_highest_claim_nonce()
return config.GLM_SENDER_NONCE if last_nonce is None else last_nonce + 1
21 changes: 21 additions & 0 deletions backend/app/core/glm.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
from app import database
from app.constants import BURN_ADDRESS
from app.contracts.erc20 import glm, gnt
from flask import current_app as app

from app.crypto.account import Account
from app.settings import config


def get_current_glm_supply() -> int:
Expand All @@ -9,3 +14,19 @@ def get_current_glm_supply() -> int:
- glm.balance_of(BURN_ADDRESS)
- gnt.balance_of(BURN_ADDRESS)
)


def transfer_claimed():
account = Account.from_key(config.GLM_SENDER_PRIVATE_KEY)
claims = database.claims.get_by_claimed_true_and_nonce_gte(account.nonce)
for claim in claims:
app.logger.debug(f"Transferring GLM to user: {claim.address}")
try:
tx_hash = glm.transfer(claim.address, claim.claim_nonce)
app.logger.info(
f"GLM transferred to user: {claim.address}, tx: {tx_hash.hex()}"
)
except Exception as e:
if "nonce too low" in str(e).lower():
continue
app.logger.error(f"Cannot transfer GLM: {e}")
1 change: 1 addition & 0 deletions backend/app/core/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from app import database
from app.core.common import AccountFunds
from app.settings import config
from app.crypto.terms_and_conditions_consent import verify_signed_message

from app.exceptions import DuplicateConsent, InvalidSignature
Expand Down
20 changes: 20 additions & 0 deletions backend/app/crypto/eip712.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,26 @@ def build_allocations_eip712_data(message: dict) -> dict:
}


def build_claim_glm_eip712_data() -> dict:
claim_glm_types = {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
],
"ClaimGLMPayload": [
{"name": "msg", "type": "string"},
],
}

return {
"types": claim_glm_types,
"domain": domain,
"primaryType": "ClaimGLMPayload",
"message": {"msg": f"Claim {int(config.GLM_WITHDRAWAL_AMOUNT / 1e18)} GLMs"},
}


def sign(account: Union[Account, LocalAccount], data: dict) -> str:
"""
Signs the provided message with w3.eth.account following EIP-712 structure
Expand Down
1 change: 1 addition & 0 deletions backend/app/database/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
finalized_epoch_snapshot,
deposits,
rewards,
claims,
user_consents,
)
39 changes: 39 additions & 0 deletions backend/app/database/claims.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from typing import List

from eth_utils import to_checksum_address
from sqlalchemy import func

from app.database.models import EpochZeroClaim
from app.extensions import db


def get_all() -> List[EpochZeroClaim]:
return EpochZeroClaim.query.all()


def get_by_address(user_address: str) -> EpochZeroClaim:
return EpochZeroClaim.query.filter_by(
address=to_checksum_address(user_address)
).first()


def add_claim(user_address: str) -> EpochZeroClaim:
claim = EpochZeroClaim(address=to_checksum_address(user_address))
db.session.add(claim)

return claim


def get_by_claimed_true_and_nonce_gte(nonce: int = 0) -> List[EpochZeroClaim]:
return (
EpochZeroClaim.query.filter(
EpochZeroClaim.claimed == True,
EpochZeroClaim.claim_nonce >= nonce,
)
.order_by(EpochZeroClaim.claim_nonce.asc())
.all()
)


def get_highest_claim_nonce() -> int:
return db.session.query(func.max(EpochZeroClaim.claim_nonce)).scalar()
10 changes: 9 additions & 1 deletion backend/app/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class User(BaseModel):

id = Column(db.Integer, primary_key=True)
address = Column(db.String(42), unique=True, nullable=False)
nonce = Column(db.Integer, nullable=False, default=0)
allocation_nonce = Column(db.Integer, nullable=False, default=0)


class UserConsents(BaseModel):
Expand Down Expand Up @@ -82,3 +82,11 @@ class Reward(BaseModel):
epoch = Column(db.Integer, nullable=False)
address = Column(db.String(42), nullable=False)
amount = Column(db.String, nullable=False)


class EpochZeroClaim(BaseModel):
__tablename__ = "epoch_zero_claims"

address = Column(db.String(42), primary_key=True, nullable=False)
claimed = Column(db.Boolean, default=False)
claim_nonce = db.Column(db.Integer(), unique=True, nullable=True)
10 changes: 6 additions & 4 deletions backend/app/database/user.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
from typing import List

from eth_utils import to_checksum_address

from app.database.models import User
from app.extensions import db


def get_all():
def get_all() -> List[User]:
return User.query.all()


def get_by_address(user_address):
def get_by_address(user_address: str) -> User:
return User.query.filter_by(address=to_checksum_address(user_address)).first()


def add_user(user_address):
def add_user(user_address: str) -> User:
user = User(address=to_checksum_address(user_address))
db.session.add(user)

return user


def get_or_add_user(user_address):
def get_or_add_user(user_address: str) -> User:
user = get_by_address(user_address)
if not user:
user = add_user(user_address)
Expand Down
16 changes: 16 additions & 0 deletions backend/app/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@ def __init__(self, address: str):
super().__init__(self.description.format(address), self.code)


class NotEligibleToClaimGLM(OctantException):
code = 403
description = "User with address: {} is not eligible to claim GLMs."

def __init__(self, user_address: str):
super().__init__(self.description.format(user_address), self.code)


class GlmClaimed(OctantException):
code = 400
description = "User with address: {} has already claimed GLMs."

def __init__(self, user_address: str):
super().__init__(self.description.format(user_address), self.code)


class InvalidSignature(OctantException):
code = 400
description = "Given signature {} is invalid or does not belong to {}"
Expand Down
1 change: 0 additions & 1 deletion backend/app/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
cors = CORS()
scheduler = APScheduler()


# Other extensions
graphql_client = Client()
w3 = Web3()
Expand Down
11 changes: 11 additions & 0 deletions backend/app/infrastructure/apscheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from app.core import vault
from app.extensions import scheduler
from app.core import glm


@scheduler.task(
Expand All @@ -16,3 +17,13 @@ def vault_confirm_withdrawals():
if app.config["VAULT_CONFIRM_WITHDRAWALS_ENABLED"]:
app.logger.debug("Confirming withdrawals in Vault contract...")
vault.confirm_withdrawals()


@scheduler.task(
"interval", id="glm-transfer", seconds=15, misfire_grace_time=900, max_instances=1
)
def glm_transfer():
with scheduler.app.app_context():
if app.config["GLM_CLAIM_ENABLED"]:
app.logger.debug("Transferring claimed GLMs...")
glm.transfer_claimed()
1 change: 1 addition & 0 deletions backend/app/infrastructure/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
deposits,
withdrawals,
allocations,
glm_claim,
epochs,
user,
)
Expand Down
Loading

0 comments on commit 07f66d0

Please sign in to comment.