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

Tokens #273

Merged
merged 51 commits into from
Jul 25, 2022
Merged

Tokens #273

merged 51 commits into from
Jul 25, 2022

Conversation

MartinMinkov
Copy link
Contributor

@MartinMinkov MartinMinkov commented Jul 5, 2022

Description

This PR adds support for custom tokens and token symbols for SnarkyJS. It implements the requirements mentioned in the following RFC: #233

Mina PR: MinaProtocol/mina#11449

To enable minting, burning, and sending, changes to the Party structure was made to support this.token()... as well as this.tokenSymbol.set(...). Additionally, changes to account fetching have been made to support querying for a specific token id in addition to a public key.

Tested

Implemented unit tests in token.test.ts to check different behaviors that tokens offer. These tests are only related to the local ledger and thus are not tested with a live network yet. To test with a live network, a new QA network should be deployed and then a zkApp deployed that acts similar to the zkApp in the test file.

@MartinMinkov MartinMinkov force-pushed the test/tokens-playground branch 6 times, most recently from 065f60b to a714246 Compare July 18, 2022 19:08
@MartinMinkov MartinMinkov marked this pull request as ready for review July 18, 2022 19:26
@MartinMinkov MartinMinkov changed the title [WIP] - Tokens Tokens Jul 18, 2022
Copy link
Collaborator

@mitschabaude mitschabaude left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

started review, will continue tomorrow!

src/lib/fetch.ts Outdated Show resolved Hide resolved
src/lib/fetch.ts Outdated Show resolved Hide resolved
src/lib/party.ts Outdated Show resolved Hide resolved
src/lib/party.ts Outdated Show resolved Hide resolved
src/lib/precondition.ts Outdated Show resolved Hide resolved
src/snarky.d.ts Outdated Show resolved Hide resolved
src/lib/mina.ts Outdated Show resolved Hide resolved
Copy link
Contributor

@ymekuria ymekuria left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work Martin! I just had a couple very minor comments/questions.

src/lib/party.ts Outdated Show resolved Hide resolved
src/lib/party.ts Outdated Show resolved Hide resolved
src/lib/state.ts Outdated Show resolved Hide resolved
Copy link
Collaborator

@mitschabaude mitschabaude left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finished reviewing! Just a couple of smaller suggestions from me -- great work!

The most important change I'm suggesting is to remove the tokenId argument in token(tokenId?), because it has to be the same as this.tokenId anyway. (Correct me if I'm wrong.)

src/lib/party.ts Outdated Show resolved Hide resolved
src/lib/party.ts Show resolved Hide resolved
src/lib/party.ts Outdated
@@ -516,8 +577,105 @@ class Party {
return new (Party as any)(body, authorization, party.isSelf);
}

token(tokenId?: Field) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we rename this argument to parentTokenId for clarity?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh wait -- doesn't the parentTokenId have to equal this.self.body.tokenId? Otherwise the this account isn't the owner and can't do this? maybe we don't need that argument after all??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is a little confusing to me, I'll have to come back to it :P

src/lib/party.ts Outdated Show resolved Hide resolved
src/lib/party.ts Outdated Show resolved Hide resolved
src/lib/party.ts Show resolved Hide resolved
src/lib/party.ts Show resolved Hide resolved
src/lib/token.test.ts Outdated Show resolved Hide resolved
src/lib/token.test.ts Outdated Show resolved Hide resolved
src/lib/token.test.ts Outdated Show resolved Hide resolved
Copy link
Collaborator

@mitschabaude mitschabaude left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm now!

src/lib/token.test.ts Show resolved Hide resolved
it('should have a valid custom token id', async () => {
const tokenId = zkapp.token().id;
const expectedTokenId = new Token({ tokenOwner: zkappAddress }).id;
expect(tokenId).toBeDefined();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could delete this line expect(tokenId).toBeDefined(); given the following assertion, no? Or is there a chance it returns undefined?


@method init() {
this.tokenSymbol.set(tokenSymbol);
this.totalAmountInCirculation.set(UInt64.zero);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should consider using the term Quantity, instead of Amount, everywhere. On trading dashboards, qty refers to the number of tokens and amount prefers to price * amount, which would be USD etc. when trading on fiat markets. So qty is more consistent with how it's used on most financial platforms.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also typically see (and we use consistently in the backend code) the term "totalSupply" for this -- probably for consistency we should go with that.

Copy link
Member

@bkase bkase left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great! I just have a few questions that either you can just explain away in comments or perhaps I found a bug

src/lib/fetch.ts Outdated
const accountQuery = (publicKey: string) => `{
account(publicKey: "${publicKey}") {
const accountQuery = (publicKey: string, tokenId: string) => `{
account(publicKey: "${publicKey}" tokenId: "${tokenId}") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't you need a comma between the publicKey and tokenId in the query?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

src/lib/mina.ts Outdated
if (ledgerAccount == undefined) {
throw new Error(
`getAccount: Could not find account for public key ${publicKey.toBase58()}`
`getAccount: Could not find account for public key ${publicKey.toBase58()} with the tokenId ${Ledger.fieldToBase58(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you omit the with the tokenId ... in the case that the tokenId is the default one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, fixed!

src/lib/mina.ts Outdated
if (account !== undefined) return account;
}
throw Error(
`getAccount: Could not find account for public key ${publicKey.toBase58()}.\nGraphql endpoint: ${graphqlEndpoint}`
`getAccount: Could not find account for public key ${publicKey.toBase58()} with the tokenId ${Ledger.fieldToBase58(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

src/lib/mina.ts Outdated
Fetch.defaultGraphqlEndpoint
);
if (account === undefined)
throw Error(
`getAccount: Could not find account for public key ${publicKey.toBase58()}. Either call Mina.setActiveInstance first or explicitly add the account with addCachedAccount`
`getAccount: Could not find account for public key ${publicKey.toBase58()} with the tokenId ${Ledger.fieldToBase58(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here (should this string be a constant stored somewhere instead of inlined all the time?)

src/lib/party.ts Outdated
tokenOwner.toFields().every((x) => x.isConstant()) &&
parentTokenId.isConstant()
) {
this.id = Ledger.customTokenIdChecked(tokenOwner, this.parentTokenId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between these branches?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey @MartinMinkov, I think these branches are wrong.. the else branch should use the checked version, not the if branch

@bkase, if the token owner or parent token id is a variable then we need to derive the token id in-snark. (Not sure if we could always use the checked version, depends on how flexible it is written, but IIRC checked hashing returns a new variable even when the inputs are constant, which wouldn't work outside the circuit)

Copy link
Contributor Author

@MartinMinkov MartinMinkov Jul 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yikes! Good catch, switched these. 😅

receiverParty.body.balanceChange =
receiverParty.body.balanceChange.sub(amount);

// Require signature from the receiver account being deducted
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this always true? Can't certain tokens be burned via proof?

Copy link
Contributor Author

@MartinMinkov MartinMinkov Jul 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, I'm not sure how this would work with the current SnarkyJS API. @mitschabaude, is there a way to require either a proof or signature? Also, how would one construct a proof to do this (in the context of a developer creating a proof with SnarkyJS)?

Copy link
Collaborator

@mitschabaude mitschabaude Jul 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aaah yes. I forgot about that. It depends on the permissions on the sending party's account whether it needs a proof or a signature.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a puzzle 🤔 because different zkApps have completely different ways of authorizing transactions. So my fear is token contracts will typically impose a certain interface on zkApps for that token.

I'm pretty sure @mrmr1993 has thought deeper about this

In any case, I think there's no easy solution here. The token() API we designed seems fundamentally not flexible enough to handle parties with proofs, because they are all different. The API must be one where users provide their own parties, not where we create parties for them. But even then it seems very tricky to make a circuit for the token owner contract that's flexible enough to handle arbitrary zkApp children. So I'm not even sure we can ever provide a ready-made, general implementation for parties with proofs.

That makes it all the more important to at least expose the low-level pieces to create token contracts -- I think that's the Token class (which I still think should be just a function)

Copy link
Collaborator

@mitschabaude mitschabaude Jul 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a fairly general solution is not that hard to build after all.
The token() functions must witness their child parties instead of hardcoding them into the circuit, and only assert the properties of them that they care about - balanceChanges and tokenId.

The challenge is how to create a good API for this. I imagine its functions must take a party as input, which might mean we have to make Party one of the possible arguments of a smart contract method. A smart contract could sort of "call" a token contract with 'this' as an argument, and that token contract would make the smart contract its child. A sort of inverted zkapp composability

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the natural API from the protocol perspective would be if the token contract methods would accept a callback, which is like a transaction block in that you can run a (or possibly multiple) smart contract methods in it, which would create parties that are authorized with a proof. And the token contract would then assert certain conditions on the resulting parties, for example

  • They can't have children
  • There must be exactly / at most X of them
  • The balances must cancel in send
  • In mint / burn, conditions X, Y, Z must hold

Copy link
Collaborator

@mitschabaude mitschabaude Jul 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a separate issue (#300) so that we can discuss the priority & path forward on this. It's out of scope for this PR, it's already nice to have token transfers authorized by a signature

senderParty.body.balanceChange =
senderParty.body.balanceChange.sub(amount);

// Require signature from the sender party
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here -- aren't there certain senders that need a proof instead?


@method init() {
this.tokenSymbol.set(tokenSymbol);
this.totalAmountInCirculation.set(UInt64.zero);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also typically see (and we use consistently in the backend code) the term "totalSupply" for this -- probably for consistency we should go with that.

@MartinMinkov MartinMinkov merged commit a4fae70 into main Jul 25, 2022
@MartinMinkov MartinMinkov deleted the test/tokens-playground branch July 25, 2022 16:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants