@lumina-dex/contracts
Version:
Contracts for Lumina DEX.
638 lines (571 loc) • 23.3 kB
text/typescript
import {
AccountUpdate,
Bool,
fetchAccount,
Field,
MerkleMap,
MerkleTree,
Mina,
Poseidon,
PrivateKey,
Provable,
PublicKey,
Signature,
UInt32,
UInt64,
UInt8
} from "o1js"
import { beforeAll, beforeEach, describe, expect, it } from "vitest"
import {
claimerNumber,
Farm,
FarmingEvent,
FarmMerkleWitness,
FarmReward,
FarmRewardTokenHolder,
FarmTokenHolder,
FungibleToken,
FungibleTokenAdmin,
minTimeUnlockFarmReward,
MultisigInfo,
Pool,
PoolFactory,
PoolTokenHolder,
SignatureInfo,
SignatureRight,
UpdateSignerData
} from "../dist"
const proofsEnabled = false
describe("Farming pool token", () => {
let deployerAccount: Mina.TestPublicKey,
deployerKey: PrivateKey,
senderAccount: Mina.TestPublicKey,
senderKey: PrivateKey,
bobAccount: Mina.TestPublicKey,
bobKey: PrivateKey,
merkle: MerkleMap,
aliceAccount: Mina.TestPublicKey,
aliceKey: PrivateKey,
dylanAccount: Mina.TestPublicKey,
bobPublic: PublicKey,
alicePublic: PublicKey,
dylanPublic: PublicKey,
senderPublic: PublicKey,
deployerPublic: PublicKey,
allRight: SignatureRight,
deployRight: SignatureRight,
zkAppAddress: PublicKey,
zkAppPrivateKey: PrivateKey,
zkApp: PoolFactory,
zkPoolAddress: PublicKey,
zkPoolPrivateKey: PrivateKey,
zkPool: Pool,
zkTokenAdminAddress: PublicKey,
zkTokenAdminPrivateKey: PrivateKey,
zkTokenAdmin: FungibleTokenAdmin,
zkTokenAddress0: PublicKey,
zkTokenPrivateKey0: PrivateKey,
zkToken0: FungibleToken,
zkTokenAddress1: PublicKey,
zkTokenPrivateKey1: PrivateKey,
zkToken1: FungibleToken,
zkTokenAddress2: PublicKey,
zkTokenPrivateKey2: PrivateKey,
zkToken2: FungibleToken,
zkFarmTokenAddress: PublicKey,
zkFarmTokenPrivateKey: PrivateKey,
zkFarmToken: Farm,
zkFarmTokenHolder: FarmTokenHolder,
local: any,
tokenHolder: PoolTokenHolder
beforeAll(async () => {
const analyze = await Pool.analyzeMethods()
getGates(analyze)
if (proofsEnabled) {
console.time("compile pool")
await FungibleTokenAdmin.compile()
await FungibleToken.compile()
await PoolFactory.compile()
await Pool.compile()
await PoolTokenHolder.compile()
await Farm.compile()
console.timeEnd("compile pool")
}
function getGates(analyze: any) {
for (const key in analyze) {
if (Object.prototype.hasOwnProperty.call(analyze, key)) {
const element = analyze[key]
console.log(key, element?.gates.length)
}
}
}
})
beforeEach(async () => {
// no transaction limits on zeko
local = await Mina.LocalBlockchain({ proofsEnabled, enforceTransactionLimits: false })
Mina.setActiveInstance(local)
;[deployerAccount, senderAccount, bobAccount, aliceAccount, dylanAccount] = local.testAccounts
deployerKey = deployerAccount.key
senderKey = senderAccount.key
bobKey = bobAccount.key
aliceKey = aliceAccount.key
senderPublic = senderKey.toPublicKey()
bobPublic = bobKey.toPublicKey()
alicePublic = aliceKey.toPublicKey()
dylanPublic = dylanAccount.key.toPublicKey()
deployerPublic = deployerKey.toPublicKey()
allRight = new SignatureRight(Bool(true), Bool(true), Bool(true), Bool(true), Bool(true), Bool(true))
deployRight = SignatureRight.canDeployPool()
zkAppPrivateKey = PrivateKey.random()
zkAppAddress = zkAppPrivateKey.toPublicKey()
zkApp = new PoolFactory(zkAppAddress)
zkPoolPrivateKey = PrivateKey.random()
zkPoolAddress = zkPoolPrivateKey.toPublicKey()
zkPool = new Pool(zkPoolAddress)
zkFarmTokenPrivateKey = PrivateKey.random()
zkFarmTokenAddress = zkFarmTokenPrivateKey.toPublicKey()
zkFarmToken = new Farm(zkFarmTokenAddress)
zkFarmTokenHolder = new FarmTokenHolder(zkFarmTokenAddress, zkPool.deriveTokenId())
zkTokenAdminPrivateKey = PrivateKey.random()
zkTokenAdminAddress = zkTokenAdminPrivateKey.toPublicKey()
zkTokenAdmin = new FungibleTokenAdmin(zkTokenAdminAddress)
const keyTokenX = PrivateKey.random()
const keyTokenY = PrivateKey.random()
// order token to create pool
const xIsLower = keyTokenX.toPublicKey().x.lessThan(keyTokenY.toPublicKey().x)
zkTokenPrivateKey0 = xIsLower.toBoolean() ? keyTokenX : keyTokenY
zkTokenAddress0 = zkTokenPrivateKey0.toPublicKey()
zkToken0 = new FungibleToken(zkTokenAddress0)
zkTokenPrivateKey1 = xIsLower.toBoolean() ? keyTokenY : keyTokenX
zkTokenAddress1 = zkTokenPrivateKey1.toPublicKey()
zkToken1 = new FungibleToken(zkTokenAddress1)
zkTokenPrivateKey2 = PrivateKey.random()
zkTokenAddress2 = zkTokenPrivateKey2.toPublicKey()
zkToken2 = new FungibleToken(zkTokenAddress2)
tokenHolder = new PoolTokenHolder(zkPoolAddress, zkToken0.deriveTokenId())
merkle = new MerkleMap()
merkle.set(Poseidon.hash(bobPublic.toFields()), allRight.hash())
merkle.set(Poseidon.hash(alicePublic.toFields()), allRight.hash())
merkle.set(Poseidon.hash(senderPublic.toFields()), allRight.hash())
merkle.set(Poseidon.hash(deployerPublic.toFields()), deployRight.hash())
const root = merkle.getRoot()
const today = new Date()
today.setDate(today.getDate() + 1)
const tomorrow = today.getTime()
const time = getSlotFromTimestamp(tomorrow)
const info = new UpdateSignerData({ oldRoot: Field.empty(), newRoot: root, deadlineSlot: UInt32.from(time) })
const signBob = Signature.create(bobKey, info.toFields())
const signAlice = Signature.create(aliceKey, info.toFields())
const signDylan = Signature.create(senderAccount.key, info.toFields())
const multi = new MultisigInfo({
approvedUpgrader: root,
messageHash: info.hash(),
deadlineSlot: UInt32.from(time)
})
const infoBob = new SignatureInfo({
user: bobPublic,
witness: merkle.getWitness(Poseidon.hash(bobPublic.toFields())),
signature: signBob,
right: allRight
})
const infoAlice = new SignatureInfo({
user: alicePublic,
witness: merkle.getWitness(Poseidon.hash(alicePublic.toFields())),
signature: signAlice,
right: allRight
})
const infoDylan = new SignatureInfo({
user: senderPublic,
witness: merkle.getWitness(Poseidon.hash(senderPublic.toFields())),
signature: signDylan,
right: allRight
})
const array = [infoBob, infoAlice, infoDylan]
const txn = await Mina.transaction(deployerAccount, async () => {
AccountUpdate.fundNewAccount(deployerAccount, 4)
await zkApp.deploy({
symbol: "FAC",
src: "https://luminadex.com/",
protocol: aliceAccount,
delegator: dylanAccount,
approvedSigner: root,
signatures: array,
multisigInfo: multi
})
await zkTokenAdmin.deploy({
adminPublicKey: deployerAccount
})
await zkToken0.deploy({
symbol: "LTA",
src: "https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts",
allowUpdates: false
})
await zkToken0.initialize(
zkTokenAdminAddress,
UInt8.from(9),
Bool(false)
)
})
await txn.prove()
// this tx needs .sign(), because `deploy()` adds an account update that requires signature authorization
await txn.sign([deployerKey, zkAppPrivateKey, zkTokenAdminPrivateKey, zkTokenPrivateKey0]).send()
// deploy token 2
const txn2 = await Mina.transaction(deployerAccount, async () => {
AccountUpdate.fundNewAccount(deployerAccount, 2)
await zkToken1.deploy({
symbol: "LTB",
src: "https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts",
allowUpdates: false
})
await zkToken1.initialize(
zkTokenAdminAddress,
UInt8.from(9),
Bool(false)
)
})
await txn2.prove()
// this tx needs .sign(), because `deploy()` adds an account update that requires signature authorization
await txn2.sign([deployerKey, zkAppPrivateKey, zkTokenAdminPrivateKey, zkTokenPrivateKey1]).send()
// deploy token 3
const txn4 = await Mina.transaction(deployerAccount, async () => {
AccountUpdate.fundNewAccount(deployerAccount, 2)
await zkToken2.deploy({
symbol: "REW",
src: "https://github.com/MinaFoundation/mina-fungible-token/blob/main/FungibleToken.ts",
allowUpdates: false
})
await zkToken2.initialize(
zkTokenAdminAddress,
UInt8.from(9),
Bool(false)
)
})
await txn4.prove()
// this tx needs .sign(), because `deploy()` adds an account update that requires signature authorization
await txn4.sign([deployerKey, zkAppPrivateKey, zkTokenAdminPrivateKey, zkTokenPrivateKey2]).send()
const signature = Signature.create(bobKey, zkPoolAddress.toFields())
const witness = merkle.getWitness(Poseidon.hash(bobPublic.toFields()))
const txn3 = await Mina.transaction(deployerAccount, async () => {
AccountUpdate.fundNewAccount(deployerAccount, 5)
await zkApp.createPoolToken(
zkPoolAddress,
zkTokenAddress0,
zkTokenAddress1,
bobAccount,
signature,
witness,
allRight
)
})
// console.log("Pool creation", txn3.toPretty());
console.log("Pool creation au", txn3.transaction.accountUpdates.length)
await txn3.prove()
// this tx needs .sign(), because `deploy()` adds an account update that requires signature authorization
await txn3.sign([deployerKey, zkPoolPrivateKey]).send()
const { genesisTimestamp, slotTime } = Mina.getNetworkConstants()
let start = BigInt(Date.now() + 10_000)
start = (start - genesisTimestamp.toBigInt()) / slotTime.toBigInt()
let end = start + BigInt(1_000_000)
end = end / slotTime.toBigInt()
const txn10 = await Mina.transaction(deployerAccount, async () => {
AccountUpdate.fundNewAccount(deployerAccount, 2)
await zkFarmToken.deploy({
owner: deployerAccount,
pool: zkPoolAddress,
startSlot: new UInt64(start),
endSlot: new UInt64(end)
})
await zkFarmTokenHolder.deploy({
owner: deployerAccount
})
await zkPool.approveAccountUpdate(zkFarmTokenHolder.self)
})
console.log("zkFarmToken deploy", txn10.toPretty())
await txn10.prove()
await txn10.sign([zkFarmTokenPrivateKey, deployerKey]).send()
// mint token to user
await mintToken(deployerAccount)
await mintToken(senderAccount)
await mintToken(bobAccount)
await mintToken(aliceAccount)
const amt = UInt64.from(10 * 10 ** 9)
const amtToken = UInt64.from(50 * 10 ** 9)
const txn5 = await Mina.transaction(senderAccount, async () => {
AccountUpdate.fundNewAccount(senderAccount, 1)
await zkPool.supplyFirstLiquiditiesToken(amt, amtToken)
})
await txn5.prove()
await txn5.sign([senderKey]).send()
const amtMina = UInt64.from(1 * 10 ** 9)
const amtToken2 = UInt64.from(5 * 10 ** 9)
let totalLiquidity = Mina.getBalance(zkPoolAddress, zkPool.deriveTokenId())
let amtToken0 = Mina.getBalance(zkPoolAddress, zkToken0.deriveTokenId())
let amtToken1 = Mina.getBalance(zkPoolAddress, zkToken1.deriveTokenId())
let txn8 = await Mina.transaction(bobAccount, async () => {
AccountUpdate.fundNewAccount(bobAccount, 1)
await zkPool.supplyLiquidityToken(amtMina, amtToken2, amtToken0, amtToken1, totalLiquidity)
})
await txn8.prove()
await txn8.sign([bobKey]).send()
totalLiquidity = Mina.getBalance(zkPoolAddress, zkPool.deriveTokenId())
amtToken0 = Mina.getBalance(zkPoolAddress, zkToken0.deriveTokenId())
amtToken1 = Mina.getBalance(zkPoolAddress, zkToken1.deriveTokenId())
txn8 = await Mina.transaction(aliceAccount, async () => {
AccountUpdate.fundNewAccount(aliceAccount, 1)
await zkPool.supplyLiquidityToken(amtMina, amtToken2, amtToken0, amtToken1, totalLiquidity)
})
await txn8.prove()
await txn8.sign([aliceKey]).send()
})
function getSlotFromTimestamp(date: number) {
const { genesisTimestamp, slotTime } = Mina.activeInstance.getNetworkConstants()
let slotCalculated = UInt64.from(date)
slotCalculated = (slotCalculated.sub(genesisTimestamp)).div(slotTime)
Provable.log("slotCalculated64", slotCalculated)
return slotCalculated.toUInt32()
}
it("deposit withdraw", async () => {
await fetchAccount({ publicKey: zkFarmTokenAddress })
let txn = await Mina.transaction(senderAccount, async () => {
AccountUpdate.fundNewAccount(senderAccount, 1)
await zkFarmToken.deposit(UInt64.from(1000))
})
await txn.prove()
console.log("zkFarmToken deposit", txn.toPretty())
await txn.sign([senderKey]).send()
const balance = Mina.getBalance(zkFarmTokenAddress, zkPool.deriveTokenId())
expect(balance.toBigInt()).toEqual(1000n)
const balanceLiquidity = Mina.getBalance(senderAccount, zkFarmToken.deriveTokenId())
expect(balanceLiquidity.toBigInt()).toEqual(1000n)
txn = await Mina.transaction(senderAccount, async () => {
await zkFarmTokenHolder.withdraw(UInt64.from(1000))
await zkPool.approveAccountUpdate(zkFarmTokenHolder.self)
})
await txn.prove()
console.log("zkFarmToken withdraw", txn.toPretty())
await txn.sign([senderKey]).send()
const balanceAfter = Mina.getBalance(zkFarmTokenAddress, zkPool.deriveTokenId())
expect(balanceAfter.toBigInt()).toEqual(0n)
const balanceLiquidityAfter = Mina.getBalance(senderAccount, zkFarmToken.deriveTokenId())
expect(balanceLiquidityAfter.toBigInt()).toEqual(0n)
})
it("get reward", async () => {
let txn = await Mina.transaction(bobAccount, async () => {
AccountUpdate.fundNewAccount(bobAccount, 1)
await zkFarmToken.deposit(UInt64.from(1000))
})
await txn.prove()
await txn.sign([bobKey]).send()
local.setGlobalSlot(1)
txn = await Mina.transaction(bobAccount, async () => {
await zkFarmTokenHolder.withdraw(UInt64.from(1000))
await zkPool.approveAccountUpdate(zkFarmTokenHolder.self)
})
await txn.prove()
await txn.sign([bobKey]).send()
local.setGlobalSlot(2)
txn = await Mina.transaction(aliceAccount, async () => {
AccountUpdate.fundNewAccount(aliceAccount, 1)
await zkFarmToken.deposit(UInt64.from(5000))
})
await txn.prove()
await txn.sign([aliceKey]).send()
local.setGlobalSlot(3)
txn = await Mina.transaction(aliceAccount, async () => {
await zkFarmToken.deposit(UInt64.from(333))
})
await txn.prove()
await txn.sign([aliceKey]).send()
local.setGlobalSlot(5)
txn = await Mina.transaction(aliceAccount, async () => {
await zkFarmTokenHolder.withdraw(UInt64.from(1333))
await zkPool.approveAccountUpdate(zkFarmTokenHolder.self)
})
await txn.prove()
await txn.sign([aliceKey]).send()
local.setGlobalSlot(10)
txn = await Mina.transaction(aliceAccount, async () => {
await zkFarmTokenHolder.withdraw(UInt64.from(4000))
await zkPool.approveAccountUpdate(zkFarmTokenHolder.self)
})
await txn.prove()
await txn.sign([aliceKey]).send()
const dataReward = await generateRewardMerkle(zkFarmTokenAddress, zkPool.deriveTokenId(), claimerNumber)
console.log("rewardList", dataReward.rewardList)
const key = PrivateKey.random()
const farmReward = new FarmReward(key.toPublicKey())
const farmRewardTokenHolder = new FarmRewardTokenHolder(key.toPublicKey(), zkToken2.deriveTokenId())
// add more token for test
const amountReward = dataReward.totalReward * 10n
const timeUnlock = UInt64.from(Date.now()).add(minTimeUnlockFarmReward.mul(2))
txn = await Mina.transaction(deployerAccount, async () => {
AccountUpdate.fundNewAccount(deployerAccount, 2)
await farmReward.deploy({
owner: deployerAccount,
merkleRoot: dataReward.merkle.getRoot(),
token: zkTokenAddress2
})
await farmRewardTokenHolder.deploy({
owner: deployerAccount,
merkleRoot: dataReward.merkle.getRoot(),
token: zkTokenAddress2
})
await zkToken2.approveAccountUpdate(farmRewardTokenHolder.self)
// we deploy and fund reward in one transaction
await zkToken2.transfer(deployerAccount, key.toPublicKey(), UInt64.from(amountReward))
})
// console.log("farmReward deploy", txn.toPretty());
await txn.prove()
await txn.sign([key, deployerKey]).send()
// bob withdraw reward
const balanceBeforeBob = Mina.getBalance(bobAccount, zkToken2.deriveTokenId())
const indexBob = dataReward.rewardList.findIndex(x => x.user === bobAccount.toBase58())
const dataBob = dataReward.rewardList[indexBob]
const witness = dataReward.merkle.getWitness(BigInt(indexBob))
const farmWitness = new FarmMerkleWitness(witness)
txn = await Mina.transaction(bobAccount, async () => {
AccountUpdate.fundNewAccount(bobAccount, 1)
await farmRewardTokenHolder.claimReward(UInt64.from(dataBob.reward), farmWitness)
await zkToken2.approveAccountUpdate(farmRewardTokenHolder.self)
})
// console.log("farmReward claim", txn.toPretty());
await txn.prove()
await txn.sign([bobKey]).send()
const balanceAfterBob = Mina.getBalance(bobAccount, zkToken2.deriveTokenId())
// bob received the correct amount
expect(balanceAfterBob.sub(balanceBeforeBob).value).toEqual(Field.from(dataBob.reward))
txn = await Mina.transaction(bobAccount, async () => {
await farmRewardTokenHolder.claimReward(UInt64.from(dataBob.reward), farmWitness)
await zkToken2.approveAccountUpdate(farmRewardTokenHolder.self)
})
await txn.prove()
// can't claim twice
await expect(txn.sign([bobKey]).send()).rejects.toThrow()
const indexAlice = dataReward.rewardList.findIndex(x => x.user === aliceAccount.toBase58())
const dataAlice = dataReward.rewardList[indexAlice]
const witnessAlice = dataReward.merkle.getWitness(BigInt(indexAlice))
const farmWitnessAlice = new FarmMerkleWitness(witnessAlice)
// bob can'claim for alice
await expect(Mina.transaction(bobAccount, async () => {
AccountUpdate.fundNewAccount(bobAccount, 1)
await farmRewardTokenHolder.claimReward(UInt64.from(dataAlice.reward), farmWitnessAlice)
await zkToken2.approveAccountUpdate(farmRewardTokenHolder.self)
})).rejects.toThrow()
// alice claim
txn = await Mina.transaction(aliceAccount, async () => {
AccountUpdate.fundNewAccount(aliceAccount, 1)
await farmRewardTokenHolder.claimReward(UInt64.from(dataAlice.reward), farmWitnessAlice)
await zkToken2.approveAccountUpdate(farmRewardTokenHolder.self)
})
await txn.prove()
await txn.sign([aliceKey]).send()
const amountDust = Field.from(amountReward).sub(Field.from(dataBob.reward)).sub(Field.from(dataAlice.reward))
const balanceContract = Mina.getBalance(key.toPublicKey(), zkToken2.deriveTokenId())
expect(balanceContract.value).toEqual(amountDust)
})
async function mintToken(user: PublicKey) {
// token are minted to original deployer, so just transfer it for test
let txn = await Mina.transaction(deployerAccount, async () => {
AccountUpdate.fundNewAccount(deployerAccount, 1)
await zkToken0.mint(user, UInt64.from(1000 * 10 ** 9))
})
await txn.prove()
await txn.sign([deployerKey, zkTokenPrivateKey0]).send()
txn = await Mina.transaction(deployerAccount, async () => {
AccountUpdate.fundNewAccount(deployerAccount, 1)
await zkToken1.mint(user, UInt64.from(1000 * 10 ** 9))
})
await txn.prove()
await txn.sign([deployerKey, zkTokenPrivateKey1]).send()
txn = await Mina.transaction(deployerAccount, async () => {
AccountUpdate.fundNewAccount(deployerAccount, 1)
await zkToken2.mint(user, UInt64.from(1000 * 10 ** 9))
})
await txn.prove()
await txn.sign([deployerKey, zkTokenPrivateKey2]).send()
}
})
type farmInfo = {
time: bigint
amount: bigint
deposit: boolean
total: bigint
reward: bigint
}
/**
* Test method don't use it on production
* based on current reward by block
* @param farmAddress
*/
export async function generateRewardMerkle(farmAddress: PublicKey, tokenId: Field, merkleSize: number) {
const farm = new Farm(farmAddress)
const events = await farm.fetchEvents(UInt32.zero, UInt32.from(1000))
const userList = new Map<string, farmInfo[]>()
generateMapReward(events, userList, "deposit")
const farmholder = new FarmTokenHolder(farmAddress, tokenId)
const eventsWithdraw = await farmholder.fetchEvents(UInt32.zero, UInt32.from(1000))
generateMapReward(eventsWithdraw, userList, "withdraw")
let totalReward = 0n
const rewardList = []
for (const key of userList.keys()) {
console.log(key)
const data = userList.get(key) as farmInfo[]
const orderData = data.sort((a: any, b: any) => Number(a.time - b.time))
for (let index = 0; index < orderData.length; index++) {
const element = orderData[index]
let timeElapsed = 0n
if (index > 0) {
const oldData = orderData[index - 1]
// time elapsed in seconds
timeElapsed = (element.time - oldData.time) / 1000n
element.total = oldData.total + element.amount
// we give 0,000 001 token by secon by amount stacked
element.reward += timeElapsed * 1_000n * oldData.total
} else {
element.total = element.amount
}
if (index === orderData.length - 1) {
rewardList.push({ user: key, reward: element.reward })
totalReward += element.reward
}
}
}
const merkle = new MerkleTree(merkleSize)
for (let index = 0; index < rewardList.length; index++) {
const element = rewardList[index]
const userKey = PublicKey.fromBase58(element.user).toFields()
const amount = new Field(element.reward)
const hash = Poseidon.hash([userKey[0], userKey[1], amount])
merkle.setLeaf(BigInt(index), hash)
}
return { totalReward, rewardList, merkle }
}
function generateMapReward(events: any, userList: Map<string, farmInfo[]>, eventType: string) {
for (let index = 0; index < events.length; index++) {
const element = events[index]
if (element.type === eventType) {
const data = element.event.data as unknown as FarmingEvent
const sender = data.sender.toBase58()
const amount = data.amount
const time = globalSlotToTimestamp(element.globalSlot)
console.log("time", time.toBigInt())
const farmInfo = {
time: time.toBigInt(),
amount: eventType === "deposit" ? amount.toBigInt() : -amount.toBigInt(),
deposit: eventType === "deposit",
total: 0n,
reward: 0n
}
if (userList.has(sender)) {
const user = userList.get(sender)
user!.push(farmInfo)
} else {
userList.set(sender, [farmInfo])
}
}
}
}
function globalSlotToTimestamp(slot: UInt32) {
const { genesisTimestamp, slotTime } = Mina.getNetworkConstants()
return UInt64.from(slot).mul(slotTime).add(genesisTimestamp)
}