Skip to content
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

Signature support for Dropper v0.2.0 in Engine API #273

Closed
zomglings opened this issue Apr 4, 2023 · 1 comment · Fixed by #275
Closed

Signature support for Dropper v0.2.0 in Engine API #273

zomglings opened this issue Apr 4, 2023 · 1 comment · Fixed by #275
Assignees
Labels
backend db enhancement New feature or request

Comments

@zomglings
Copy link
Collaborator

zomglings commented Apr 4, 2023

Currently, the /drops endpoints on Engine API only support Dropper v0.1.0.

Now that Dropper v0.2.0 is on the main branch of this repository, we need to support signatures for the new contract on the API, as well.

Changes to database and API

Current schemas

The Dropper API is consumed mainly via the /drops/batch endpoint, which returns responses in this format:

[
  {
    "claimant": "0xc0ffee254729296a45a3885639AC7E10F9d54979",
    "claim_id": 1337,
    "title": "Drop title",
    "description": "Drop description",
    "amount": 900000000000000000000,
    "amount_string": "900000000000000000000",
    "block_deadline": 41385128,
    "signature": "b29b5e823042de122eff6c7648123ff622dbf9aa85967e09a9dd5bdb51922d8b60029619391dfb22773663aa79a8d5b430a516adecbae87bae2e2ba2afb252aaaa",
    "dropper_claim_id": "7d9238b3-e9ec-4ff6-a6de-094f67c00d19",
    "dropper_contract_address": "0x6bc613A25aFe159b70610b64783cA51C9258b92e",
    "blockchain": "polygon"
  }
]

The data is stored in the following Postgres relations (represented here by SQLAlchemy models):

class DropperContract(Base):  # type: ignore
    __tablename__ = "dropper_contracts"
    __table_args__ = (UniqueConstraint("blockchain", "address"),)

    id = Column(
        UUID(as_uuid=True),
        primary_key=True,
        default=uuid.uuid4,
        unique=True,
        nullable=False,
    )
    blockchain = Column(VARCHAR(128), nullable=False)
    address = Column(VARCHAR(256), index=True)
    title = Column(VARCHAR(128), nullable=True)
    description = Column(String, nullable=True)
    image_uri = Column(String, nullable=True)

    created_at = Column(
        DateTime(timezone=True), server_default=utcnow(), nullable=False
    )
    updated_at = Column(
        DateTime(timezone=True),
        server_default=utcnow(),
        onupdate=utcnow(),
        nullable=False,
    )


class DropperClaim(Base):  # type: ignore
    __tablename__ = "dropper_claims"

    id = Column(
        UUID(as_uuid=True),
        primary_key=True,
        default=uuid.uuid4,
        unique=True,
        nullable=False,
    )
    dropper_contract_id = Column(
        UUID(as_uuid=True),
        ForeignKey("dropper_contracts.id", ondelete="CASCADE"),
        nullable=False,
    )
    claim_id = Column(BigInteger, nullable=True)
    title = Column(VARCHAR(128), nullable=True)
    description = Column(String, nullable=True)
    terminus_address = Column(VARCHAR(256), nullable=True, index=True)
    terminus_pool_id = Column(BigInteger, nullable=True, index=True)
    claim_block_deadline = Column(BigInteger, nullable=True)
    active = Column(Boolean, default=False, nullable=False)

    created_at = Column(
        DateTime(timezone=True), server_default=utcnow(), nullable=False
    )
    updated_at = Column(
        DateTime(timezone=True),
        server_default=utcnow(),
        onupdate=utcnow(),
        nullable=False,
    )

    __table_args__ = (
        Index(
            "uq_dropper_claims_dropper_contract_id_claim_id",
            "dropper_contract_id",
            "claim_id",
            unique=True,
            postgresql_where=and_(claim_id.isnot(None), active.is_(True)),
        ),
    )


class DropperClaimant(Base):  # type: ignore
    __tablename__ = "dropper_claimants"
    __table_args__ = (UniqueConstraint("dropper_claim_id", "address"),)

    id = Column(
        UUID(as_uuid=True),
        primary_key=True,
        default=uuid.uuid4,
        unique=True,
        nullable=False,
    )
    dropper_claim_id = Column(
        UUID(as_uuid=True),
        ForeignKey("dropper_claims.id", ondelete="CASCADE"),
        nullable=False,
    )
    address = Column(VARCHAR(256), nullable=False, index=True)
    amount = Column(BigInteger, nullable=False)
    raw_amount = Column(String, nullable=True)
    added_by = Column(VARCHAR(256), nullable=False, index=True)
    signature = Column(String, nullable=True, index=True)

    created_at = Column(
        DateTime(timezone=True), server_default=utcnow(), nullable=False
    )
    updated_at = Column(
        DateTime(timezone=True),
        server_default=utcnow(),
        onupdate=utcnow(),
        nullable=False,
    )

How Dropper v0.2.0 differs from Dropper v0.1.0

The biggest change from v0.1.0 to v0.2.0 is in the signatures of claimMessageHash and claim.

The new claim ABI is:

  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "dropId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "requestID",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "blockDeadline",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "signer",
        "type": "address"
      },
      {
        "internalType": "bytes",
        "name": "signature",
        "type": "bytes"
      }
    ],
    "name": "claim",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }

The new claim method adds the following arguments:

  1. requestID - The same (dropId, requestID) pair cannot be claimed more than once. This replaces the previous protection on the contract, which prevented the same (claimId, msg.sender) pair from claiming more than once. The reason we have done this is so that a single drop can be used to distribute tokens to the same user multiple times. This is important for gaming use cases such as XP grants.
  2. signer - Dropper v0.2.0 uses ownership of Terminus tokens by signers to determine if signatures are valid proofs that a certain caller can claim a given drop. Since any signer holding the Terminus token specific to a drop can claim as part of that drop, the caller needs to specify which signer signed their authorization as part of the claim method. In Dropper v0.1.0, there was a single signer for each drop at any given time, which is why this argument was not needed in the claim method for that implementation.

In general, with Dropper v0.2.0 we want the contract to be the source of truth about:

  1. Who can authorize claimants to receive tokens from drops (at the API level).
  2. Whether a drop is active or not.

In the present version of the API, we use a secondary (Terminus address, Terminus tokenId) pair to determine who can post authorizations to the API. In the version for Dropper v0.2.0 we will use the same (Terminus address, Terminus tokenId) pair that is used to designate authorized signers on the smart contract.

We will not store any information about whether Dropper v0.2.0 drops are active or not at the API level. That behavior will depend only on the state of the smart contract (unlike the current Dropper v0.1.0 API).

authorized_calls

My preference would be to add a general AuthorizedCall model which would correspond to the following response schema on the Dropper v0.2.0 equivalent of /drops/batch:

[
  {
    "caller": "0xc0ffee254729296a45a3885639AC7E10F9d54979",
    "method": "claim",
    "args": {
        "dropId": 1337,
        "requestID": "42",
        "blockDeadline": "41385128",
        "amount": "8945902384543",
        "signer": "0x622b214bF13f631aa5160ed135E233C9ddb86111",
        "signature": "b29b5e823042de122eff6c7648123ff622dbf9aa85967e09a9dd5bdb51922d8b60029619391dfb22773663aa79a8d5b430a516adecbae87bae2e2ba2afb252aaaa",
    }
    "contract_type": "dropper-v0.2.0",
    "contract_address": "0x6bc613A25aFe159b70610b64783cA51C9258b92e",
    "chain_id": "137"
  }
]

There would be an enum on valid contract_type values, with each value corresponding to the appropriate authorization check based on the on-chain state of the contract.

This implies a model similar to this for AuthorizedCall (i.e. the authorized_calls relation in the database):

class AuthorizedCall(Base):
    __tablename__ = "authorized_calls"
   
    id = Column(
        UUID(as_uuid=True),
        primary_key=True,
        default=uuid.uuid4,
        unique=True,
        nullable=False,
    )

    caller = Column(Integer, nullable=False, index=True)
    chain_id = Column(String, nullable=False, index=True)
    contract_type = Column(String, nullable=False, index=True) # Really an enum over valid contract types. Currently: "dropper-v0.2.0"
    contract_address = Column(Integer, nullable=False, index=True)

    method = Column(String, nullable=False, index=True)
    method_args = Column(JSONB, nullable=False)

We may want to make a joint index on some or all of the indexed columns above (e.g. (chain_id, contract_address, contract_type) could be a joint index).

Authentication on POST endpoint

The API endpoint where administrators POST authorizations for drops will check the Authorization header of each request for a message signed by an account which bears the same Terminus token that acts as the authorization token for the drop with the given dropId.

@zomglings zomglings self-assigned this Apr 4, 2023
@zomglings zomglings removed the design label Apr 4, 2023
@zomglings
Copy link
Collaborator Author

Updates based on conversation with @kompotkot and @Andrei-Dolgolev :

  • Authentication should be based on Moonstream accounts. For each authorized_calls row, we should store a Moonstream user ID representing the authorizer. This is the only approach that generalizes to arbitrary contract calls. Eventually, we will add a subscription check when a user POSTs an authorization to the API.
  • We should probably put contract information in a separate table (shitty working name: authorized_call_contracts) so that we don't duplicate, (chain_id, contract_address, contract_type) a crazy amount in the authorized_calls table.
  • New contract type: raw. Empty method. method_args will contain "calldata". Arbitrary contract calls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backend db enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant