Skip to content

Commit

Permalink
impl: mina deposit & withdraw
Browse files Browse the repository at this point in the history
  • Loading branch information
berzanorg committed Apr 30, 2024
1 parent b7b5b40 commit 39b97ea
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 56 deletions.
92 changes: 86 additions & 6 deletions bridge-contract/src/bridge-contract.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {
AccountUpdate,
Field,
MerkleMapWitness,
Nullifier,
Permissions,
Poseidon,
PublicKey,
Expand All @@ -19,9 +17,11 @@ import {
SingleBurnWitness,
SingleWithdrawalWitness,
Withdrawal,
choose,
} from "nacho-common-o1js"
import { SafeContract } from "./safe-contract.js"
import { TokenContract } from "nacho-token-contract"
import { RollupContract } from "nacho-rollup-contract"

export class BridgeContract extends SmartContract {
reducer = Reducer({
Expand All @@ -42,10 +42,11 @@ export class BridgeContract extends SmartContract {

this.account.permissions.set({
...Permissions.allImpossible(),
access: Permissions.proof(),
access: Permissions.none(),
editActionState: Permissions.proof(),
editState: Permissions.proof(),
incrementNonce: Permissions.proof(),
receive: Permissions.none(),
send: Permissions.proof(),
setPermissions: Permissions.proof(),
})
Expand Down Expand Up @@ -80,6 +81,25 @@ export class BridgeContract extends SmartContract {
this.emitEvent("deposited", deposit)
}

@method addMinaDeposit(amount: UInt64) {
const au = AccountUpdate.createSigned(this.sender)

au.send({
to: this.address,
amount,
})

const deposit = new Deposit({
depositor: this.sender,
tokenId: Field(1), // Mina's Token ID.
tokenAmount: amount,
})

this.reducer.dispatch(Poseidon.hash(deposit.toFields()))

this.emitEvent("deposited", deposit)
}

@method applyDeposits() {
const actionState = this.actionState.getAndRequireEquals()
const depositsMerkleListRoot = this.depositsMerkleListRoot.getAndRequireEquals()
Expand All @@ -101,7 +121,7 @@ export class BridgeContract extends SmartContract {
}

@method withdrawTokens(
singleWithdrawWitness: SingleWithdrawalWitness,
singleWithdrawalWitness: SingleWithdrawalWitness,
singleBurnWitness: SingleBurnWitness,
tokenContractPublicKey: PublicKey,
totalWithdrawAmount: UInt64,
Expand All @@ -112,7 +132,7 @@ export class BridgeContract extends SmartContract {
const safeContract = new SafeContract(this.address, tokenId)

safeContract.checkAndSubBalance(
singleWithdrawWitness,
singleWithdrawalWitness,
singleBurnWitness,
tokenContractPublicKey,
totalWithdrawAmount,
Expand All @@ -125,7 +145,67 @@ export class BridgeContract extends SmartContract {
tokenContract.transfer(safeContract.self, this.sender, amount)

this.withdrawalsMerkleTreeRoot.set(
singleWithdrawWitness.calculateRoot(
singleWithdrawalWitness.calculateRoot(
Poseidon.hash([...this.sender.toFields(), tokenId, amount.value]),
),
)

this.emitEvent(
"withdrawn",
new Withdrawal({
withdrawer: this.sender,
tokenId,
tokenAmount: totalBurnAmount,
}),
)
}

@method withdrawMina(
singleWithdrawalWitness: SingleWithdrawalWitness,
singleBurnWitness: SingleBurnWitness,
totalWithdrawAmount: UInt64,
totalBurnAmount: UInt64,
) {
const tokenId = Field(1)

// We don't have to check if burned amount is greater than withdrawn amount as it throws an underflow error if it isn't.
const amount = totalBurnAmount.sub(totalWithdrawAmount)

// NOTE: We require that both burn and withdraw leaves point to the same index.
singleWithdrawalWitness.calculateIndex().assertEquals(singleBurnWitness.calculateIndex())

const bridgeContract = new BridgeContract(this.address)
const withdrawalsMerkleTreeRoot =
bridgeContract.withdrawalsMerkleTreeRoot.getAndRequireEquals()
const rollupContractPublicKey = bridgeContract.rollupContractPublicKey.getAndRequireEquals()

const rollupContract = new RollupContract(rollupContractPublicKey)

const stateRoots = rollupContract.stateRoots.getAndRequireEquals()

const burn = new Burn({
burner: this.sender,
tokenId,
tokenAmount: totalBurnAmount,
})
const burnHash = Poseidon.hash(burn.toFields())

const withdrawHash = choose(
totalWithdrawAmount.equals(UInt64.zero),
Field(0),
Poseidon.hash(burn.toFields()),
)

stateRoots.burns.assertEquals(singleBurnWitness.calculateRoot(burnHash))
withdrawalsMerkleTreeRoot.assertEquals(singleWithdrawalWitness.calculateRoot(withdrawHash))

this.send({
to: this.sender,
amount,
})

this.withdrawalsMerkleTreeRoot.set(
singleWithdrawalWitness.calculateRoot(
Poseidon.hash([...this.sender.toFields(), tokenId, amount.value]),
),
)
Expand Down
107 changes: 57 additions & 50 deletions bridge-contract/tests/bridge-contract.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it } from "node:test"
import assert from "assert"
import { AccountUpdate, Bool, Mina, Poseidon, Signature, UInt64, MerkleTree } from "o1js"
import { AccountUpdate, Mina, Poseidon, Signature, UInt64, MerkleTree, Field } from "o1js"
import { BridgeContract } from "../src/bridge-contract.js"
import { Deposit, SingleWithdrawalWitness, WITHDRAWALS_TREE_HEIGHT } from "nacho-common-o1js"
import { RollupContract } from "nacho-rollup-contract"
Expand All @@ -19,18 +19,12 @@ describe("bridge contract", async () => {
const LocalBlockchain = Mina.LocalBlockchain({ proofsEnabled: false })
Mina.setActiveInstance(LocalBlockchain)

const minaTokenContractKeypair = generateKeypair()
const usdcTokenContractKeypair = generateKeypair()
const rollupContractKeypair = generateKeypair()
const bridgeContractKeypair = generateKeypair()
const minaTokenContract = new TokenContract(minaTokenContractKeypair.publicKey)
const usdcTokenContract = new TokenContract(usdcTokenContractKeypair.publicKey)
const rollupContract = new RollupContract(rollupContractKeypair.publicKey)
const bridgeContract = new BridgeContract(bridgeContractKeypair.publicKey)
const minaSafeContract = new SafeContract(
bridgeContractKeypair.publicKey,
minaTokenContract.deriveTokenId(),
)
const usdcSafeContract = new SafeContract(
bridgeContractKeypair.publicKey,
usdcTokenContract.deriveTokenId(),
Expand All @@ -43,37 +37,27 @@ describe("bridge contract", async () => {

it("deploys mina and usdc token contracts", async () => {
const tx = await Mina.transaction(john.publicKey, () => {
minaTokenContract.deploy()
usdcTokenContract.deploy()
AccountUpdate.fundNewAccount(john.publicKey, 2)
AccountUpdate.fundNewAccount(john.publicKey)
})

tx.sign([
john.privateKey,
minaTokenContractKeypair.privateKey,
usdcTokenContractKeypair.privateKey,
])
tx.sign([john.privateKey, usdcTokenContractKeypair.privateKey])

await tx.prove()
await tx.send()
})

it("mints mina and usdc tokens", async () => {
const tx = await Mina.transaction(john.publicKey, () => {
minaTokenContract.mint(UInt64.from(20_000_000), john.publicKey)
usdcTokenContract.mint(UInt64.from(200_000_000), john.publicKey)
AccountUpdate.fundNewAccount(john.publicKey, 2)
AccountUpdate.fundNewAccount(john.publicKey)
})

tx.sign([john.privateKey])

await tx.prove()
await tx.send()

Mina.getBalance(john.publicKey, minaTokenContract.deriveTokenId()).assertEquals(
UInt64.from(20_000_000),
)

Mina.getBalance(john.publicKey, usdcTokenContract.deriveTokenId()).assertEquals(
UInt64.from(200_000_000),
)
Expand All @@ -94,7 +78,7 @@ describe("bridge contract", async () => {
it("deploys bridge contract", async () => {
const tx = await Mina.transaction(john.publicKey, () => {
bridgeContract.deploy()
minaTokenContract.approveAccountUpdate(bridgeContract.self)
usdcTokenContract.approveAccountUpdate(bridgeContract.self)
AccountUpdate.fundNewAccount(john.publicKey)
})

Expand All @@ -106,11 +90,9 @@ describe("bridge contract", async () => {

it("deploys safe contracts of bridge", async () => {
const tx = await Mina.transaction(john.publicKey, () => {
minaSafeContract.deploy()
usdcSafeContract.deploy()
minaTokenContract.approveAccountUpdate(minaSafeContract.self)
usdcTokenContract.approveAccountUpdate(usdcSafeContract.self)
AccountUpdate.fundNewAccount(john.publicKey, 2)
AccountUpdate.fundNewAccount(john.publicKey)
})

tx.sign([john.privateKey, bridgeContractKeypair.privateKey])
Expand Down Expand Up @@ -138,22 +120,17 @@ describe("bridge contract", async () => {

it("adds deposits", async () => {
const tx = await Mina.transaction(john.publicKey, () => {
bridgeContract.addDeposit(minaTokenContractKeypair.publicKey, UInt64.from(10_000_000))
bridgeContract.addMinaDeposit(UInt64.from(10_000_000))
})

tx.sign([john.privateKey])

await tx.prove()
await tx.send()

Mina.getBalance(john.publicKey, minaTokenContract.deriveTokenId()).assertEquals(
UInt64.from(10_000_000),
)
Mina.getBalance(john.publicKey).assertEquals(UInt64.from(994_990_000_000))

Mina.getBalance(
bridgeContractKeypair.publicKey,
minaTokenContract.deriveTokenId(),
).assertEquals(UInt64.from(10_000_000))
Mina.getBalance(bridgeContractKeypair.publicKey).assertEquals(UInt64.from(10_000_000))
})

it("adds deposits one more time", async () => {
Expand Down Expand Up @@ -195,7 +172,7 @@ describe("bridge contract", async () => {
deposits[0],
new Deposit({
depositor: john.publicKey,
tokenId: minaTokenContract.deriveTokenId(),
tokenId: Field(1),
tokenAmount: UInt64.from(10_000_000),
}),
)
Expand Down Expand Up @@ -272,31 +249,23 @@ describe("bridge contract", async () => {
const currentBalance = UInt64.from(10_000_000)
const currentBurn = UInt64.from(0)
const amountToBurn = UInt64.from(10_000_000)
const userSignature = Signature.create(john.privateKey, [
minaTokenContract.deriveTokenId(),
amountToBurn.value,
])
const userSignature = Signature.create(john.privateKey, [Field(1), amountToBurn.value])

const proof = await proofGenerator.makeBurnTokens(
stateUtil.stateRoots,
stateUtil.lastProof,
stateUtil.getSingleBalanceWitness(0n),
stateUtil.getSingleBurnWitness(0n),
john.publicKey,
minaTokenContract.deriveTokenId(),
Field(1),
currentBurn,
currentBalance,
amountToBurn,
userSignature,
)

stateUtil.setBalance(
0n,
john.publicKey,
minaTokenContract.deriveTokenId(),
currentBalance.sub(amountToBurn),
)
stateUtil.setBurn(0n, john.publicKey, minaTokenContract.deriveTokenId(), amountToBurn)
stateUtil.setBalance(0n, john.publicKey, Field(1), currentBalance.sub(amountToBurn))
stateUtil.setBurn(0n, john.publicKey, Field(1), amountToBurn)

stateUtil.pushProof(proof)
})
Expand Down Expand Up @@ -369,7 +338,7 @@ describe("bridge contract", async () => {
await tx.send()
})

it("withdraws tokens", async () => {
it("withdraws mina", async () => {
const totalWithdrawAmount = UInt64.from(0)
const totalBurnAmount = UInt64.from(10_000_000)
const singleWithdrawalWitness = new SingleWithdrawalWitness(
Expand All @@ -379,10 +348,9 @@ describe("bridge contract", async () => {
)

const tx = await Mina.transaction(john.publicKey, () => {
bridgeContract.withdrawTokens(
bridgeContract.withdrawMina(
singleWithdrawalWitness,
stateUtil.getSingleBurnWitness(0n),
minaTokenContractKeypair.publicKey,
totalWithdrawAmount,
totalBurnAmount,
)
Expand All @@ -395,10 +363,49 @@ describe("bridge contract", async () => {

withdrawalsTree.setLeaf(
0n,
Poseidon.hash([...john.publicKey.toFields(), Field(1), totalBurnAmount.value]),
)
})

it("withdraws usdc", async () => {
const totalWithdrawAmount = UInt64.from(0)
const totalBurnAmount = UInt64.from(100_000_000)
const singleWithdrawalWitness = new SingleWithdrawalWitness(
withdrawalsTree
.getWitness(1n)
.map((a) => ({ isLeft: !a.isLeft, value: a.sibling.toBigInt() })),
)

const tx = await Mina.transaction(john.publicKey, () => {
bridgeContract.withdrawTokens(
singleWithdrawalWitness,
stateUtil.getSingleBurnWitness(1n),
usdcTokenContract.address,
totalWithdrawAmount,
totalBurnAmount,
)
})

tx.sign([john.privateKey])

await tx.prove()
await tx.send()

Mina.getBalance(john.publicKey, usdcTokenContract.deriveTokenId()).assertEquals(
UInt64.from(200_000_000),
)

Mina.getBalance(
bridgeContractKeypair.publicKey,
usdcTokenContract.deriveTokenId(),
).assertEquals(UInt64.from(0))

withdrawalsTree.setLeaf(
1n,
Poseidon.hash([
...john.publicKey.toFields(),
minaTokenContract.deriveTokenId(),
totalWithdrawAmount.value,
usdcTokenContract.deriveTokenId(),
totalBurnAmount.value,
]),
)
})
Expand Down

0 comments on commit 39b97ea

Please sign in to comment.