Skip to content

Commit

Permalink
update: Token implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
berzanorg committed Dec 23, 2023
1 parent 28a92a3 commit 0bcbec0
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 113 deletions.
189 changes: 111 additions & 78 deletions contracts/src/Token.test.ts
Original file line number Diff line number Diff line change
@@ -1,115 +1,148 @@
import { AccountUpdate, Field, Mina, PrivateKey, PublicKey, UInt64, Encoding, Experimental } from 'o1js'
import {
AccountUpdate,
DeployArgs,
Encoding,
Experimental,
Field,
Int64,
Mina,
Permissions,
PrivateKey,
PublicKey,
SmartContract,
State,
UInt64,
VerificationKey,
method,
state,
} from 'o1js'
import { Token } from './Token'

const proofsEnabled = false

describe('Token Contract', () => {
const Local = Mina.LocalBlockchain({ proofsEnabled })
Mina.setActiveInstance(Local)
const createRandomAccount = () => {
const privateKey = PrivateKey.random()
const publicKey = privateKey.toPublicKey()
return {
publicKey,
privateKey,
}
}

const deployerPrivateKey: PrivateKey = Local.testAccounts[0].privateKey
const deployerPublicKey: PublicKey = Local.testAccounts[0].publicKey
const Local = Mina.LocalBlockchain({ proofsEnabled })
Mina.setActiveInstance(Local)

const userPrivateKey: PrivateKey = Local.testAccounts[1].privateKey
const userPublicKey: PublicKey = Local.testAccounts[1].publicKey
const accounts = {
token: createRandomAccount(),
feePayer: Local.testAccounts[0],
mainUser: Local.testAccounts[1],
randomUser: createRandomAccount(),
}

const tokenZkAppPrivateKey: PrivateKey = PrivateKey.random()
const tokenZkAppPublicKey: PublicKey = tokenZkAppPrivateKey.toPublicKey()
const token = new Token(accounts.token.publicKey)

const token: Token = new Token(tokenZkAppPublicKey)
const { verificationKey: verificationKeyOfToken } = await Token.compile()

let verificationKey: { data: string; hash: Field }
it('can deploy tokens', async () => {
const tx = await Mina.transaction(accounts.feePayer.publicKey, () => {
AccountUpdate.fundNewAccount(accounts.feePayer.publicKey)

beforeAll(async () => {
verificationKey = (await Token.compile()).verificationKey
token.deploy({ verificationKey: verificationKeyOfToken })
})

it('can create a new token', async () => {
const tx = await Mina.transaction(deployerPublicKey, () => {
AccountUpdate.fundNewAccount(deployerPublicKey)
token.deploy({ verificationKey, zkappKey: tokenZkAppPrivateKey })
})
await tx.prove()

await tx.prove()
await tx.sign([deployerPrivateKey, tokenZkAppPrivateKey]).send()
tx.sign([accounts.feePayer.privateKey, accounts.token.privateKey])

await tx.send()

expect(token.decimals.get()).toEqual(UInt64.zero)
expect(token.symbol.get()).toEqual(Field(0))
expect(token.maxSupply.get()).toEqual(UInt64.zero)
expect(token.circulatingSupply.get()).toEqual(UInt64.zero)
})

it('can initialize tokens', async () => {
const symbol = Encoding.stringToFields('MYT')[0]
const decimals = UInt64.from(3)
const maxSupply = UInt64.from(100_000_000)

const tx = await Mina.transaction(accounts.feePayer.publicKey, () => {
token.initialize(symbol, decimals, maxSupply)
})

it('can initialize token ', async () => {
const symbol = Encoding.stringToFields('MYT')[0]
const decimals = UInt64.from(3)
const maxSupply = UInt64.from(100_000_000)
await tx.prove()

tx.sign([accounts.feePayer.privateKey, accounts.token.privateKey])

const tx = await Mina.transaction(deployerPublicKey, () => {
token.initialize(symbol, decimals, maxSupply)
})
await tx.send()

await tx.prove()
await tx.sign([deployerPrivateKey, tokenZkAppPrivateKey]).send()
expect(token.decimals.get()).toEqual(decimals)
expect(token.symbol.get()).toEqual(symbol)
expect(token.maxSupply.get()).toEqual(maxSupply)
expect(token.circulatingSupply.get()).toEqual(UInt64.from(0))
})

console.log(token.decimals.get().toString())
it('can mint tokens', async () => {
const amount = UInt64.from(100_000_000)
const receiver = accounts.mainUser.publicKey

expect(token.decimals.get()).toEqual(decimals)
expect(token.symbol.get()).toEqual(symbol)
expect(token.maxSupply.get()).toEqual(maxSupply)
expect(token.circulatingSupply.get()).toEqual(UInt64.from(0))
const tx = await Mina.transaction(accounts.feePayer.publicKey, () => {
AccountUpdate.fundNewAccount(accounts.feePayer.publicKey)
token.mint(receiver, amount)
})

it('can mint tokens', async () => {
const receiverAddress = userPublicKey
const amount = UInt64.from(50_000_000)
await tx.prove()

const tx = await Mina.transaction(deployerPublicKey, () => {
AccountUpdate.fundNewAccount(deployerPublicKey)
token.mint(receiverAddress, amount)
})
tx.sign([accounts.feePayer.privateKey, accounts.token.privateKey])

await tx.prove()
await tx.sign([deployerPrivateKey]).send()
await tx.send()

expect(Mina.getBalance(receiverAddress, token.token.id)).toEqual(amount)
const receiverBalance = Mina.getBalance(receiver, token.token.id)
expect(receiverBalance).toEqual(amount)
})

it('can transfer tokens', async () => {
const senderStartingBalance = UInt64.from(100_000_000)
const transferAmount = UInt64.from(10_000_000)
const sender = accounts.mainUser.publicKey
const receiver = accounts.randomUser.publicKey

const senderPrivateKey = accounts.mainUser.privateKey

const tx = await Mina.transaction(sender, () => {
AccountUpdate.fundNewAccount(sender)
token.transfer(sender, receiver, transferAmount)
})

it('can send and receive tokens', async () => {
const sender = userPublicKey
const receiver = PrivateKey.random().toPublicKey()
const amount = UInt64.from(20_000_000)
const remainingAmount = UInt64.from(80_000_000)
await tx.prove()

const tx = await Mina.transaction(sender, () => {
AccountUpdate.fundNewAccount(sender)
token.transfer(sender, receiver, amount)
})
tx.sign([senderPrivateKey])

await tx.prove()
await tx.sign([userPrivateKey]).send()
await tx.send()

expect(Mina.getBalance(receiver, token.token.id)).toEqual(amount)
expect(Mina.getBalance(sender, token.token.id)).toEqual(remainingAmount)
})
const senderBalance = Mina.getBalance(sender, token.token.id)
expect(senderBalance).toEqual(senderStartingBalance.sub(transferAmount))

it("can't send and receive tokens if balance is not enough", async () => {
const receiverAddress = PrivateKey.random().toPublicKey()
const amount = UInt64.from(90_000_000)
const receiverBalance = Mina.getBalance(receiver, token.token.id)
expect(receiverBalance).toEqual(transferAmount)
})

it("can't transfer tokens when not signed by the sender", async () => {
const transferAmount = UInt64.from(10_000_000)
const sender = accounts.mainUser.publicKey
const receiver = accounts.randomUser.publicKey

const tx = await Mina.transaction(deployerPublicKey, () => {
AccountUpdate.fundNewAccount(deployerPublicKey)
token.transfer(deployerPublicKey, receiverAddress, amount)
})
const anyOtherPrivateKey = PrivateKey.random()

await tx.prove()
await tx.sign([deployerPrivateKey]).send()
const tx = await Mina.transaction(sender, () => {
AccountUpdate.fundNewAccount(sender)
token.transfer(sender, receiver, transferAmount)
})

it("can't send and receive tokens if not signed by the sender", async () => {
const receiverAddress = PrivateKey.random().toPublicKey()
const amount = UInt64.from(10_000_000)
await tx.prove()

const tx = await Mina.transaction(deployerPublicKey, () => {
AccountUpdate.fundNewAccount(deployerPublicKey)
token.transfer(deployerPublicKey, receiverAddress, amount)
})
tx.sign([anyOtherPrivateKey])

await tx.prove()
await tx.sign([]).send()
})
expect(tx.send()).rejects.toThrow()
})
76 changes: 41 additions & 35 deletions contracts/src/Token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,8 @@ import {
state,
} from 'o1js'

/**
*
* # `Token` Smart Contract
*
* The smart contract for tokens on Xane.
*
* **Note:** `deploy` method requires two account funding.
*
* # Usage
*
* ```ts
* // Import `Token` smart contract.
* import { Token } from 'xane'
*
* // Create an instance of `Token` contract.
* const token = new Token(zkAppPublicKey)
*
* // Deploy it.
* token.deploy({ verificationKey, zkappKey })
*
*
* ```
*
*/
enum TokenError {
MaxSupplyCannotBeExceeded = 'TOKEN: Max supply cannot be exceeded.',
}

export class Token extends SmartContract {
@state(UInt64) symbol = State<Field>()
@state(Field) symbol = State<Field>()
@state(UInt64) decimals = State<UInt64>()
@state(UInt64) maxSupply = State<UInt64>()
@state(UInt64) circulatingSupply = State<UInt64>()
Expand All @@ -52,17 +24,14 @@ export class Token extends SmartContract {

this.account.permissions.set({
...Permissions.default(),
editState: Permissions.proof(),
send: Permissions.proof(),
access: Permissions.proof(),
access: Permissions.proofOrSignature(),
})
}

@method initialize(symbol: Field, decimals: UInt64, maxSupply: UInt64) {
this.symbol.set(symbol)
this.decimals.set(decimals)
this.maxSupply.set(maxSupply)
this.circulatingSupply.set(UInt64.from(0))
}

@method mint(receiver: PublicKey, amount: UInt64) {
Expand All @@ -71,7 +40,7 @@ export class Token extends SmartContract {

const newCirculatingSupply = circulatingSupply.add(amount)

newCirculatingSupply.assertLessThanOrEqual(maxSupply, TokenError.MaxSupplyCannotBeExceeded)
newCirculatingSupply.assertLessThanOrEqual(maxSupply)

this.token.mint({
address: receiver,
Expand All @@ -98,7 +67,7 @@ export class Token extends SmartContract {
this.token.send({ from: sender, to: receiver, amount })
}

@method transferWithCallback(
@method approveCallbackAndTransfer(
sender: PublicKey,
receiver: PublicKey,
amount: UInt64,
Expand All @@ -107,6 +76,7 @@ export class Token extends SmartContract {
const tokenId = this.token.id

const senderAccountUpdate = this.approve(callback, AccountUpdate.Layout.AnyChildren)

senderAccountUpdate.body.tokenId.assertEquals(tokenId)
senderAccountUpdate.body.publicKey.assertEquals(sender)

Expand All @@ -116,4 +86,40 @@ export class Token extends SmartContract {
const receiverAccountUpdate = Experimental.createChildAccountUpdate(this.self, receiver, tokenId)
receiverAccountUpdate.balance.addInPlace(amount)
}

@method approveUpdateAndTransfer(zkappUpdate: AccountUpdate, receiver: PublicKey, amount: UInt64) {
// TODO: THIS IS INSECURE. The proper version has a prover error (compile != prove) that must be fixed
this.approve(zkappUpdate, AccountUpdate.Layout.AnyChildren)

// THIS IS HOW IT SHOULD BE DONE:
// // approve a layout of two grandchildren, both of which can't inherit the token permission
// let { StaticChildren, AnyChildren } = AccountUpdate.Layout;
// this.approve(zkappUpdate, StaticChildren(AnyChildren, AnyChildren));
// zkappUpdate.body.mayUseToken.parentsOwnToken.assertTrue();
// let [grandchild1, grandchild2] = zkappUpdate.children.accountUpdates;
// grandchild1.body.mayUseToken.inheritFromParent.assertFalse();
// grandchild2.body.mayUseToken.inheritFromParent.assertFalse();

// see if balance change cancels the amount sent
const balanceChange = Int64.fromObject(zkappUpdate.body.balanceChange)
balanceChange.assertEquals(Int64.from(amount).neg())

const receiverAccountUpdate = Experimental.createChildAccountUpdate(this.self, receiver, this.token.id)
receiverAccountUpdate.balance.addInPlace(amount)
}

@method approveUpdate(zkappUpdate: AccountUpdate) {
this.approve(zkappUpdate)
const balanceChange = Int64.fromObject(zkappUpdate.body.balanceChange)
balanceChange.assertEquals(Int64.from(0))
}

// Instead, use `approveUpdate` method.
// @method deployZkapp(address: PublicKey, verificationKey: VerificationKey) {
// let tokenId = this.token.id
// let zkapp = AccountUpdate.create(address, tokenId)
// zkapp.account.permissions.set(Permissions.default())
// zkapp.account.verificationKey.set(verificationKey)
// zkapp.requireSignature()
// }
}

0 comments on commit 0bcbec0

Please sign in to comment.