Skip to content

Commit

Permalink
OCT-1520 & OCT-1522: allow storing allocation signatures and provide …
Browse files Browse the repository at this point in the history
…proper hashes to db (#127)
  • Loading branch information
leoni-q authored Apr 8, 2024
2 parents 7e35444 + babd480 commit 1dad5ac
Show file tree
Hide file tree
Showing 34 changed files with 709 additions and 246 deletions.
7 changes: 7 additions & 0 deletions backend/app/infrastructure/contracts/abi.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,13 @@
"stateMutability": "view",
"type": "function",
},
{
"inputs": [{"internalType": "bytes", "name": "message", "type": "bytes"}],
"name": "getMessageHash",
"outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}],
"stateMutability": "view",
"type": "function",
},
]

DEPOSITS = [
Expand Down
3 changes: 3 additions & 0 deletions backend/app/infrastructure/contracts/gnosis_safe.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ def is_valid_signature(self, msg_hash: str, signature: str) -> bool:

result = self.contract.functions.isValidSignature(msg_hash, signature).call()
return result == bytes.fromhex(EIP1271_MAGIC_VALUE_BYTES)

def get_message_hash(self, message: bytes) -> str:
return self.contract.functions.getMessageHash(message).call()
4 changes: 3 additions & 1 deletion backend/app/infrastructure/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ class MultisigSignatures(BaseModel):
address = Column(db.String(42), nullable=False)
type = Column(db.String, nullable=False)
message = Column(db.String, nullable=False)
hash = Column(db.String, nullable=False)
msg_hash = Column(db.String, nullable=False)
safe_msg_hash = Column(db.String, nullable=False)
status = Column(db.String, nullable=False)
user_ip = Column(db.String, nullable=False)
confirmed_signature = Column(db.String, nullable=True)
28 changes: 24 additions & 4 deletions backend/app/infrastructure/database/multisig_signature.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dataclasses import dataclass
from enum import StrEnum
from typing import Optional
from typing import Optional, List

from app.extensions import db
from app.infrastructure.database.models import MultisigSignatures
Expand All @@ -11,6 +12,21 @@ class SigStatus(StrEnum):
APPROVED = "approved"


@dataclass
class MultisigFilters:
type: SignatureOpType | None
status: SigStatus | None

@property
def filters(self):
filters = {}
if self.type:
filters["type"] = self.type.value
if self.status:
filters["status"] = self.status.value
return filters


def get_last_pending_signature(
user_address: str, op_type: SignatureOpType
) -> Optional[MultisigSignatures]:
Expand All @@ -21,23 +37,27 @@ def get_last_pending_signature(
return last_signature.first()


def get_all_pending_signatures() -> list[MultisigSignatures]:
return MultisigSignatures.query.filter_by(status=SigStatus.PENDING).all()
def get_multisig_signatures_by_filters(
filters: MultisigFilters,
) -> List[MultisigSignatures]:
return MultisigSignatures.query.filter_by(**filters.filters).all()


def save_signature(
user_address: str,
op_type: SignatureOpType,
message: str,
msg_hash: str,
safe_msg_hash: str,
user_ip: str,
status: SigStatus = SigStatus.PENDING,
):
signature = MultisigSignatures(
address=user_address,
type=op_type,
message=message,
hash=msg_hash,
msg_hash=msg_hash,
safe_msg_hash=safe_msg_hash,
user_ip=user_ip,
status=status,
)
Expand Down
3 changes: 2 additions & 1 deletion backend/app/legacy/crypto/eip712.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@

from app.extensions import w3
from app.modules.dto import UserAllocationPayload
from app.modules.common.signature import encode_for_signing, EncodingStandardFor
from app.modules.common.crypto.signature import encode_for_signing, EncodingStandardFor


# TODO remove crypto package from legacy: https://linear.app/golemfoundation/issue/OCT-1523/clean-up-crypto-legacy-package
def build_domain():
return {
"name": "Octant",
Expand Down
5 changes: 4 additions & 1 deletion backend/app/legacy/crypto/eth_sign/signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@

from app.legacy.crypto.account import is_contract
from app.legacy.crypto.eip1271 import is_valid_signature
from app.modules.common.signature import hash_signable_message, EncodingStandardFor
from app.modules.common.crypto.signature import (
hash_signable_message,
EncodingStandardFor,
)


def verify_signed_message(user_address: str, msg_text: str, signature: str) -> bool:
Expand Down
27 changes: 27 additions & 0 deletions backend/app/modules/common/allocation_deserializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from typing import Dict, List

from eth_utils import to_checksum_address

from app.engine.projects.rewards import AllocationItem
from app.modules.dto import UserAllocationRequestPayload, UserAllocationPayload


def deserialize_payload(payload: Dict) -> UserAllocationRequestPayload:
allocation_payload = payload["payload"]
allocation_items = deserialize_items(allocation_payload)
nonce = int(allocation_payload["nonce"])
signature = payload.get("signature")

return UserAllocationRequestPayload(
payload=UserAllocationPayload(allocation_items, nonce), signature=signature
)


def deserialize_items(payload: Dict) -> List[AllocationItem]:
return [
AllocationItem(
proposal_address=to_checksum_address(allocation_data["proposalAddress"]),
amount=int(allocation_data["amount"]),
)
for allocation_data in payload["allocations"]
]
19 changes: 19 additions & 0 deletions backend/app/modules/common/crypto/eip1271.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from hexbytes import HexBytes

from app.extensions import w3
from app.infrastructure.contracts import abi
from app.infrastructure.contracts.gnosis_safe import GnosisSafe


def is_valid_signature(address: str, msg_hash: str, signature: str) -> bool:
contract = GnosisSafe(
w3=w3, contract=w3.eth.contract(address=address, abi=abi.GNOSIS_SAFE)
)
return contract.is_valid_signature(msg_hash, signature)


def get_message_hash(address: str, safe_message_hash: str) -> str:
contract = GnosisSafe(
w3=w3, contract=w3.eth.contract(address=address, abi=abi.GNOSIS_SAFE)
)
return f"0x{contract.get_message_hash(HexBytes(safe_message_hash)).hex()}"
80 changes: 80 additions & 0 deletions backend/app/modules/common/crypto/eip712.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from typing import Union

from eth_account import Account
from eth_account.signers.local import LocalAccount
from flask import current_app as app

from app.extensions import w3
from app.modules.dto import UserAllocationPayload
from app.modules.common.crypto.signature import encode_for_signing, EncodingStandardFor


def build_allocations_eip712_structure(payload: UserAllocationPayload):
message = {}
message["allocations"] = [
{"proposalAddress": a.proposal_address, "amount": a.amount}
for a in payload.allocations
]
message["nonce"] = payload.nonce
return build_allocations_eip712_data(message)


def build_allocations_eip712_data(message: dict) -> dict:
domain = _build_domain()

# Convert amount value to int
message["allocations"] = [
{**allocation, "amount": int(allocation["amount"])}
for allocation in message["allocations"]
]

allocation_types = {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
],
"Allocation": [
{"name": "proposalAddress", "type": "address"},
{"name": "amount", "type": "uint256"},
],
"AllocationPayload": [
{"name": "allocations", "type": "Allocation[]"},
{"name": "nonce", "type": "uint256"},
],
}

return {
"types": allocation_types,
"domain": domain,
"primaryType": "AllocationPayload",
"message": message,
}


def sign(account: Union[Account, LocalAccount], data: dict) -> str:
"""
Signs the provided message with w3.eth.account following EIP-712 structure
:returns signature as a hexadecimal string.
"""
return account.sign_message(
encode_for_signing(EncodingStandardFor.DATA, data)
).signature.hex()


def recover_address(data: dict, signature: str) -> str:
"""
Recovers the address from EIP-712 structured data
:returns address as a hexadecimal string.
"""
return w3.eth.account.recover_message(
encode_for_signing(EncodingStandardFor.DATA, data), signature=signature
)


def _build_domain():
return {
"name": "Octant",
"version": "1.0.0",
"chainId": app.config["CHAIN_ID"],
}
File renamed without changes.
9 changes: 6 additions & 3 deletions backend/app/modules/facades/confirm_multisig.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@ def confirm_multisig():

for tos_signature in approvals.tos_signatures:
post_user_terms_of_service_consent(
tos_signature.user_address, tos_signature.hash, tos_signature.ip_address
tos_signature.user_address,
tos_signature.signature,
tos_signature.ip_address,
)
apply_pending_tos_signature(tos_signature.id)

for allocation_signature in approvals.allocation_signatures:
message = json.loads(allocation_signature.message)
message["signature"] = allocation_signature.signature
allocate(
allocation_signature.user_address,
message["payload"],
is_manually_edited=message["is_manually_edited"],
message,
is_manually_edited=message.get("isManuallyEdited"),
)
apply_pending_allocation_signature(allocation_signature.id)
7 changes: 5 additions & 2 deletions backend/app/modules/modules_factory/current.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
HistoryService,
MultisigSignatures,
UserTos,
UserAllocationNonceProtocol,
)
from app.modules.modules_factory.protocols import SimulatePendingSnapshots
from app.modules.multisig_signatures.service.offchain import OffchainMultisigSignatures
from app.modules.octant_rewards.service.calculated import CalculatedOctantRewards
from app.modules.snapshots.pending.service.simulated import SimulatedPendingSnapshots
from app.modules.staking.proceeds.service.estimated import EstimatedStakingProceeds
from app.modules.user.allocations.nonce.service.saved import SavedUserAllocationsNonce
from app.modules.user.allocations.service.saved import SavedUserAllocations
from app.modules.user.deposits.service.calculated import CalculatedUserDeposits
from app.modules.user.events_generator.service.db_and_graph import (
Expand All @@ -34,7 +36,7 @@ class CurrentUserDeposits(UserEffectiveDeposits, TotalEffectiveDeposits, Protoco


class CurrentServices(Model):
user_allocations_service: SavedUserAllocations
user_allocations_nonce_service: UserAllocationNonceProtocol
user_deposits_service: CurrentUserDeposits
user_tos_service: UserTos
octant_rewards_service: OctantRewards
Expand Down Expand Up @@ -70,6 +72,7 @@ def create(chain_id: int) -> "CurrentServices":
effective_deposits=user_deposits, octant_rewards=octant_rewards
)
user_allocations = SavedUserAllocations()
user_allocations_nonce = SavedUserAllocationsNonce()
user_withdrawals = FinalizedWithdrawals()
tos_verifier = InitialUserTosVerifier()
user_tos = InitialUserTos(verifier=tos_verifier)
Expand All @@ -85,7 +88,7 @@ def create(chain_id: int) -> "CurrentServices":
verifiers={SignatureOpType.TOS: tos_verifier}, is_mainnet=is_mainnet
)
return CurrentServices(
user_allocations_service=user_allocations,
user_allocations_nonce_service=user_allocations_nonce,
user_deposits_service=user_deposits,
octant_rewards_service=CalculatedOctantRewards(
staking_proceeds=EstimatedStakingProceeds(),
Expand Down
3 changes: 3 additions & 0 deletions backend/app/modules/modules_factory/pending.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from app.modules.snapshots.finalized.service.simulated import (
SimulatedFinalizedSnapshots,
)
from app.modules.user.allocations.nonce.service.saved import SavedUserAllocationsNonce
from app.modules.user.allocations.service.pending import (
PendingUserAllocations,
PendingUserAllocationsVerifier,
Expand Down Expand Up @@ -79,7 +80,9 @@ def create(chain_id: int) -> "PendingServices":
events_based_patron_mode = EventsBasedUserPatronMode()
octant_rewards = PendingOctantRewards(patrons_mode=events_based_patron_mode)
saved_user_budgets = SavedUserBudgets()
user_nonce = SavedUserAllocationsNonce()
allocations_verifier = PendingUserAllocationsVerifier(
user_nonce=user_nonce,
user_budgets=saved_user_budgets,
patrons_mode=events_based_patron_mode,
)
Expand Down
10 changes: 9 additions & 1 deletion backend/app/modules/modules_factory/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,9 @@ def get_last_pending_signature(
) -> Signature:
...

def approve_pending_signatures(self, context: Context) -> list[Signature]:
def approve_pending_signatures(
self, context: Context, op_type: SignatureOpType
) -> list[Signature]:
...

def apply_staged_signatures(self, context: Context, signature_id: int):
Expand Down Expand Up @@ -207,3 +209,9 @@ def add_user_terms_of_service_consent(
ip_address: str,
):
...


@runtime_checkable
class UserAllocationNonceProtocol(Protocol):
def get_user_next_nonce(self, user_address: str) -> int:
...
13 changes: 8 additions & 5 deletions backend/app/modules/multisig_signatures/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
from app.exceptions import InvalidEpoch


def get_last_pending_signature(
user_address: str, op_type: SignatureOpType
) -> Signature:
def get_last_pending_signature(user_address: str, op_type: SignatureOpType) -> dict:
context = _get_context(op_type)
service = get_services(context.epoch_state).multisig_signatures_service
signature = service.get_last_pending_signature(context, user_address, op_type)

return service.get_last_pending_signature(context, user_address, op_type)
return (
{"message": signature.message, "hash": signature.safe_msg_hash}
if signature
else {}
)


def save_pending_signature(
Expand Down Expand Up @@ -62,7 +65,7 @@ def _approve(op_type: SignatureOpType) -> list[Signature]:

service = get_services(context.epoch_state).multisig_signatures_service

return service.approve_pending_signatures(context)
return service.approve_pending_signatures(context, op_type)


def _get_context(op_type: SignatureOpType) -> Context:
Expand Down
Loading

0 comments on commit 1dad5ac

Please sign in to comment.