o1-labs / o1js

TypeScript framework for zk-SNARKs and zkApps
https://docs.minaprotocol.com/en/zkapps/how-to-write-a-zkapp
Apache License 2.0
504 stars 112 forks source link

Cannot transfer custom tokens more than once #1540

Open qwadratic opened 6 months ago

qwadratic commented 6 months ago

Cannot transfer custom tokens more than once

For some reason, there is an implicit precondition on the sender account update (with tokenId of custom token) that requires the nonce to be always zero.

Here is the code to reproduce. To save your time, I precooked a test address, deployed and minted some tokens. This code for transfer worked out only for the first time. It doesn't work anymore because of that nonce precondition

import { AccountUpdate, Mina, PrivateKey, Provable, PublicKey, TokenId, UInt64, fetchAccount } from "o1js"
import { FungibleToken } from "mina-fungible-token"

const url = "https://proxy.berkeley.minaexplorer.com/graphql"

const berkeley = Mina.Network(url)
Mina.setActiveInstance(berkeley)

const fee = 1e8

const deployerKey = PrivateKey.fromBase58("EKE5nJtRFYVWqrCfdpqJqKKdt2Sskf5Co2q8CWJKEGSg71ZXzES7")
const deployer = deployerKey.toPublicKey()

const billy = PrivateKey.randomKeypair()

const contract = PublicKey.fromBase58('B62qjUrGPK1vePNUNkKfnNfxoAodfdzesBBwHfRfeQuBdxWp9kVMy7n')

console.log(`
  deployer ${deployer.toBase58()}
  billy ${billy.publicKey.toBase58()}
  contract ${contract.toBase58()}
`)

await FungibleToken.compile()
const token = new FungibleToken(contract)

console.log("[1] Transfer tokens to Billy.")

const transferTx1 = await Mina.transaction({
  sender: deployer,
  fee,
}, () => {
  AccountUpdate.fundNewAccount(deployer, 1)
  token.transfer(deployer, billy.publicKey, UInt64.from(1e9))
})

await transferTx1.prove()

transferTx1.sign([deployerKey])
const transferTxResult1 = await transferTx1.send()
console.log("Transfer tx 1:", transferTxResult1.hash)

console.log('Transfer tx 1 Account Updates')
for (let au of transferTx1.transaction.accountUpdates) {
  console.log(au.publicKey.toBase58(), TokenId.toBase58(au.tokenId))
  console.log(au.toPretty().preconditions)
  console.log(au.toPretty().update)
}

await transferTxResult1.wait()
Logs
``` deployer B62qmVz7pPiLXPvz2nPkuK3K5akjrePAVtdBVMfeyixrccgqKTQte8K billy B62qk5pcWnducEnxrCRJ2y5XexKfrRQGN7GSjjUy7VGmQ6V9FikBRxr contract B62qjUrGPK1vePNUNkKfnNfxoAodfdzesBBwHfRfeQuBdxWp9kVMy7n [1] Transfer tokens to Billy. Transfer tx 1: 5Jtx7dkRLWq1rBrCi5Ut41kKs1eahDJxjL9XiD9eMQ8EpFZGYTSk Transfer tx 1 Account Updates B62qmVz7pPiLXPvz2nPkuK3K5akjrePAVtdBVMfeyixrccgqKTQte8K wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf undefined undefined B62qjUrGPK1vePNUNkKfnNfxoAodfdzesBBwHfRfeQuBdxWp9kVMy7n wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf undefined undefined B62qjUrGPK1vePNUNkKfnNfxoAodfdzesBBwHfRfeQuBdxWp9kVMy7n wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf undefined undefined B62qmVz7pPiLXPvz2nPkuK3K5akjrePAVtdBVMfeyixrccgqKTQte8K woxTkXc9zNDiy8kWoYGXzifVZmUrZyQj7rz6qRZzqzSAwrRXTS { account: '{"nonce":{"lower":"0","upper":"0"}}' } undefined B62qqEgTgtFo4fM8LNrQzbiwhFZopZgbjf7ioykVBb2j49w3dPaWHc3 woxTkXc9zNDiy8kWoYGXzifVZmUrZyQj7rz6qRZzqzSAwrRXTS undefined undefined /Users/i/mina/mip-token-standard/node_modules/o1js/dist/node/bindings/compiled/_node_bindings/o1js_node.bc.cjs:6799 throw err; ^ Error: Transaction failed with errors: [[["Cancelled"]],[["Cancelled"]],[["Cancelled"]],[["Account_nonce_precondition_unsatisfied"]],[["Cancelled"]]] at Object.wait (file:///Users/i/mina/mip-token-standard/node_modules/o1js/dist/node/lib/mina.js:190:27) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at async file:///Users/i/mina/mip-token-standard/examples/transfer-berkeley.eg.ts:36:1 Node.js v21.1.0 ```
qwadratic commented 5 months ago

Not sure why, but the correct way is to use .internal.send()

.transfer doesn't work, because it uses requireSignature which sets nonce precondition to 0 all the time.

.internal.send() uses setLazySignature instead

not sure whats the difference.

mitschabaude commented 5 months ago

This is a bug, requireSignature() should fetch and use the correct nonce