From de79c857b1b7fca3c6de1c25b019fde071e3138d Mon Sep 17 00:00:00 2001 From: jsonDoge <16711523+jsonDoge@users.noreply.github.com> Date: Mon, 30 Dec 2024 15:06:10 +0200 Subject: [PATCH] fix: constant product differences in pool and circuit -> swap tests passing --- anchor/Cargo.lock | 39 ---- anchor/programs/darklake/Cargo.toml | 3 +- anchor/programs/darklake/src/errors.rs | 4 +- .../src/instructions/confidential_swap.rs | 60 ++++-- anchor/target/idl/darklake.json | 11 +- anchor/target/types/darklake.ts | 11 +- anchor/tests/jest/darklake.spec.ts | 187 +++++++++++++++++- anchor/tests/jest/proof.ts | 2 - 8 files changed, 247 insertions(+), 70 deletions(-) diff --git a/anchor/Cargo.lock b/anchor/Cargo.lock index 0a4c8be..805ad29 100644 --- a/anchor/Cargo.lock +++ b/anchor/Cargo.lock @@ -1213,7 +1213,6 @@ dependencies = [ "groth16-solana", "mpl-token-metadata", "solana-program 2.0.11", - "spl-math", ] [[package]] @@ -1794,12 +1793,6 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - [[package]] name = "histogram" version = "0.6.9" @@ -4441,20 +4434,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "spl-math" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc5a6cc7a4f0cf7813ce44153bba73280909f697d7f6baf7b9f223a255e7887" -dependencies = [ - "borsh 1.5.1", - "num-derive 0.4.2", - "num-traits", - "solana-program 2.0.11", - "thiserror", - "uint", -] - [[package]] name = "spl-memo" version = "4.0.0" @@ -4749,12 +4728,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strsim" version = "0.8.0" @@ -5208,18 +5181,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "uint" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" -dependencies = [ - "byteorder", - "crunchy", - "hex", - "static_assertions", -] - [[package]] name = "unicode-ident" version = "1.0.13" diff --git a/anchor/programs/darklake/Cargo.toml b/anchor/programs/darklake/Cargo.toml index 6092ccc..9f982b1 100644 --- a/anchor/programs/darklake/Cargo.toml +++ b/anchor/programs/darklake/Cargo.toml @@ -21,5 +21,4 @@ anchor-lang = { version = "0.30.1", features = ["init-if-needed"] } anchor-spl = "0.30.1" groth16-solana = "0.0.3" solana-program = "2.0.1" -mpl-token-metadata = "4.1.2" -spl-math = { version = "0.3", features = ["no-entrypoint"] } \ No newline at end of file +mpl-token-metadata = "4.1.2" \ No newline at end of file diff --git a/anchor/programs/darklake/src/errors.rs b/anchor/programs/darklake/src/errors.rs index 7c64c00..d43f2c0 100644 --- a/anchor/programs/darklake/src/errors.rs +++ b/anchor/programs/darklake/src/errors.rs @@ -24,8 +24,10 @@ pub enum ErrorCode { InvalidMetadataAccount, #[msg("Pool reserve and public signals mismatch")] PublicSignalAndPoolReserveMismatch, + #[msg("Proof input not equal to pool input")] + PoolInputAmountMismatch, #[msg("Proof amount received exceeds pool output")] - PoolAmountOutputTooLow, + PoolOutputAmountTooLow, #[msg("Unable to parse public signals")] InvalidPublicSignals, #[msg("LP mint already initialized")] diff --git a/anchor/programs/darklake/src/instructions/confidential_swap.rs b/anchor/programs/darklake/src/instructions/confidential_swap.rs index 3a3491e..28019f0 100644 --- a/anchor/programs/darklake/src/instructions/confidential_swap.rs +++ b/anchor/programs/darklake/src/instructions/confidential_swap.rs @@ -4,7 +4,6 @@ use anchor_spl::token_interface::{ transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked, }; use groth16_solana::{self, groth16::Groth16Verifier}; -use spl_math::checked_ceil_div::CheckedCeilDiv; use crate::constants::VERIFYINGKEY; use crate::errors::ErrorCode; @@ -80,13 +79,20 @@ pub fn swap( swap_source_amount: u128, swap_destination_amount: u128, ) -> Option { + msg!("source_amount: {}", source_amount); let invariant = swap_source_amount.checked_mul(swap_destination_amount)?; let new_swap_source_amount = swap_source_amount.checked_add(source_amount)?; - let (new_swap_destination_amount, new_swap_source_amount) = - invariant.checked_ceil_div(new_swap_source_amount)?; + + // round up with checked math + let qoutient = invariant.checked_div(new_swap_source_amount)?; + let remainder = invariant.checked_rem(new_swap_source_amount)?; + let new_swap_destination_amount = qoutient.checked_add(if remainder > 0 { 1 } else { 0 })?; + let source_amount_swapped = new_swap_source_amount.checked_sub(swap_source_amount)?; + msg!("source_amount_swapped: {}, new_swap_source_amount: {}, swap_source_amount: {}", source_amount_swapped, new_swap_source_amount, swap_source_amount); + let destination_amount_swapped = map_zero_to_none(swap_destination_amount.checked_sub(new_swap_destination_amount)?)?; @@ -160,12 +166,6 @@ impl<'info> ConfidentialSwap<'info> { // let new_balance_y = u64::from_be_bytes(public_signals[5][24..].try_into().unwrap()); let amount_received = u64::from_be_bytes(public_signals[2][24..].try_into().unwrap()); - msg!( - "Proof input X: {} | output Y {}", - input_amount, - amount_received - ); - // Calculate the output amount using the constant product formula let output_amount: SwapWithoutFeesResult = if is_swap_x_to_y { // Swap X to Y @@ -185,15 +185,26 @@ impl<'info> ConfidentialSwap<'info> { .ok_or(ErrorCode::MathOverflow)? }; + msg!( + "Proof input: {} | pool input {}", + input_amount, + (output_amount.source_amount_swapped as u64) + ); + + msg!( "Proof output: {} | pool output: {}", amount_received, (output_amount.destination_amount_swapped as u64) ); - // Sanity check which should be true if the circuit is correct + // Both sanity checks which should never happen if circuit and pool correctly implemented + if (output_amount.source_amount_swapped as u64) != input_amount { + return Err(ErrorCode::PoolInputAmountMismatch.into()); + } + if (output_amount.destination_amount_swapped as u64) < amount_received { - return Err(ErrorCode::PoolAmountOutputTooLow.into()); + return Err(ErrorCode::PoolOutputAmountTooLow.into()); } // Update pool reserves @@ -223,8 +234,22 @@ impl<'info> ConfidentialSwap<'info> { pool.reserve_y ); + let pool_mint_x_key = self.pool.token_mint_x.key(); + let pool_mint_y_key = self.pool.token_mint_y.key(); + + let pool_seeds = &[ + &b"pool"[..], + pool_mint_x_key.as_ref(), + pool_mint_y_key.as_ref(), + &[self.pool.bump], + ]; + + let signer_seeds = &[&pool_seeds[..]]; + + // Transfer tokens if is_swap_x_to_y { + msg!("Transferring user X to pool"); transfer_checked( CpiContext::new( self.token_mint_x_program.to_account_info(), @@ -239,20 +264,23 @@ impl<'info> ConfidentialSwap<'info> { self.token_mint_x.decimals, )?; + msg!("Transferring pool Y to user"); transfer_checked( - CpiContext::new( + CpiContext::new_with_signer( self.token_mint_y_program.to_account_info(), TransferChecked { from: self.pool_token_account_y.to_account_info(), mint: self.token_mint_y.to_account_info(), to: self.user_token_account_y.to_account_info(), - authority: pool.to_account_info(), + authority: self.pool.to_account_info(), }, + signer_seeds ), output_amount.destination_amount_swapped as u64, self.token_mint_y.decimals, )?; } else { + msg!("Transferring user Y to pool"); transfer_checked( CpiContext::new( self.token_mint_y_program.to_account_info(), @@ -267,15 +295,17 @@ impl<'info> ConfidentialSwap<'info> { self.token_mint_y.decimals, )?; + msg!("Transferring pool X to user"); transfer_checked( - CpiContext::new( + CpiContext::new_with_signer( self.token_mint_x_program.to_account_info(), TransferChecked { from: self.pool_token_account_x.to_account_info(), mint: self.token_mint_x.to_account_info(), to: self.user_token_account_x.to_account_info(), - authority: pool.to_account_info(), + authority: self.pool.to_account_info(), }, + signer_seeds ), output_amount.destination_amount_swapped as u64, self.token_mint_x.decimals, diff --git a/anchor/target/idl/darklake.json b/anchor/target/idl/darklake.json index ee61f28..b1862c9 100644 --- a/anchor/target/idl/darklake.json +++ b/anchor/target/idl/darklake.json @@ -1628,16 +1628,21 @@ }, { "code": 6011, - "name": "PoolAmountOutputTooLow", - "msg": "Proof amount received exceeds pool output" + "name": "PoolInputAmountMismatch", + "msg": "Proof input not equal to pool input" }, { "code": 6012, + "name": "PoolOutputAmountTooLow", + "msg": "Proof amount received exceeds pool output" + }, + { + "code": 6013, "name": "InvalidPublicSignals", "msg": "Unable to parse public signals" }, { - "code": 6013, + "code": 6014, "name": "LpMintAlreadyInitialized", "msg": "LP mint already initialized" } diff --git a/anchor/target/types/darklake.ts b/anchor/target/types/darklake.ts index 0c4c396..f4bff6e 100644 --- a/anchor/target/types/darklake.ts +++ b/anchor/target/types/darklake.ts @@ -1634,16 +1634,21 @@ export type Darklake = { }, { "code": 6011, - "name": "poolAmountOutputTooLow", - "msg": "Proof amount received exceeds pool output" + "name": "poolInputAmountMismatch", + "msg": "Proof input not equal to pool input" }, { "code": 6012, + "name": "poolOutputAmountTooLow", + "msg": "Proof amount received exceeds pool output" + }, + { + "code": 6013, "name": "invalidPublicSignals", "msg": "Unable to parse public signals" }, { - "code": 6013, + "code": 6014, "name": "lpMintAlreadyInitialized", "msg": "LP mint already initialized" } diff --git a/anchor/tests/jest/darklake.spec.ts b/anchor/tests/jest/darklake.spec.ts index 5a14915..d1863ea 100644 --- a/anchor/tests/jest/darklake.spec.ts +++ b/anchor/tests/jest/darklake.spec.ts @@ -139,7 +139,7 @@ describe('darklake', () => { expect(poolAccount.tokenMintY.equals(tokenY)).toBe(true); }); - describe.skip('Add Liquidity', () => { + describe('Add Liquidity', () => { const amountX = 1_000_000; // 1 token with 6 decimals const amountY = 2_000_000_000; // 2 tokens with 9 decimals @@ -208,7 +208,7 @@ describe('darklake', () => { }); describe('Swap', () => { - it('Confidential Swap Exp', async () => { + it('Confidential Swap X -> Y', async () => { const initialLiquidityX = 1_000_000; const initialLiquidityY = 2_000_000_000; // Add liquidity @@ -249,14 +249,14 @@ describe('darklake', () => { const poolAccount = await program.account.pool.fetch(poolPubkey); const publicInputs = { - inputAmount: '100000', // 0.1 token of tokenX + inputAmount: '100000', // 0.1 token of tokenX 1e5 reserveX: poolAccount.reserveX.toString(), reserveY: poolAccount.reserveY.toString(), isSwapXtoY: 1, // Swapping tokenX for tokenY }; const privateInputs = { - privateMinReceived: '100000', // Adjust this based on your expected output + privateMinReceived: '100000', // Adjust this based on your expected output }; await swap( @@ -270,10 +270,187 @@ describe('darklake', () => { privateInputs, ); + try { + const [userTokenAccountX, userTokenAccountY] = + await getOrCreateAssociatedTokenAccountsMulti( + provider.connection, + false, + payer, + payer.publicKey, + [TOKEN_X, TOKEN_Y], + ); + + const userAccountXAfterSwap = await getAccount( + provider.connection, + userTokenAccountX.address, + undefined, + tokenXProgramId, + ); + const userAccountYAfterSwap = await getAccount( + provider.connection, + userTokenAccountY.address, + undefined, + tokenYProgramId, + ); + + const expectedAmountReceived = 181818181; + + expect(Number(userAccountXAfterSwap.amount)).toEqual(fundAmountX - 1e5); + expect(Number(userAccountYAfterSwap.amount)).toEqual(expectedAmountReceived); + + const [poolTokenAccountX, poolTokenAccountY] = + await getOrCreateAssociatedTokenAccountsMulti( + provider.connection, + true, + payer, + poolPubkey, + [TOKEN_X, TOKEN_Y], + ); + + const poolAccountXAfterSwap = await getAccount( + provider.connection, + poolTokenAccountX.address, + undefined, + tokenXProgramId, + ); + const poolAccountYAfterSwap = await getAccount( + provider.connection, + poolTokenAccountY.address, + undefined, + tokenYProgramId, + ); + + expect(Number(poolAccountXAfterSwap.amount)).toEqual(initialLiquidityX + 1e5); + expect(Number(poolAccountYAfterSwap.amount)).toEqual(initialLiquidityY - expectedAmountReceived); + } catch (error) { + console.error('Error performing confidential swap:', error); + throw error; + } + }, 10000000); + + + it('Confidential Swap Y -> X', async () => { + const initialLiquidityX = 1_000_000; + const initialLiquidityY = 2_000_000_000; + // Add liquidity + await fundTokenAccounts( + provider.connection, + payer, + TOKEN_X, + TOKEN_Y, + initialLiquidityX, + initialLiquidityY, + ); + + await addLiquidity( + provider.connection, + program, + payer, + poolPubkey, + TOKEN_X, + TOKEN_Y, + initialLiquidityX, + initialLiquidityY, + ); + + // Fund user with swap from amount X + + const fundAmountX = 0; + const fundAmountY = 1_000_000_000; + await fundTokenAccounts( + provider.connection, + payer, + TOKEN_X, + TOKEN_Y, + fundAmountX, + fundAmountY, + ); + + // Swap + + const poolAccount = await program.account.pool.fetch(poolPubkey); + const publicInputs = { + inputAmount: '100000000', // 0.1 token of tokenY 1e8 + reserveX: poolAccount.reserveX.toString(), + reserveY: poolAccount.reserveY.toString(), + isSwapXtoY: 0, // Swapping tokenY for tokenX + }; + + const privateInputs = { + privateMinReceived: '0', // Adjust this based on your expected output + }; + + await swap( + provider.connection, + program, + payer, + poolPubkey, + TOKEN_X, + TOKEN_Y, + publicInputs, + privateInputs, + ); + + try { + const [userTokenAccountX, userTokenAccountY] = + await getOrCreateAssociatedTokenAccountsMulti( + provider.connection, + false, + payer, + payer.publicKey, + [TOKEN_X, TOKEN_Y], + ); + + const userAccountXAfterSwap = await getAccount( + provider.connection, + userTokenAccountX.address, + undefined, + tokenXProgramId, + ); + const userAccountYAfterSwap = await getAccount( + provider.connection, + userTokenAccountY.address, + undefined, + tokenYProgramId, + ); + + const expectedAmountReceived = 47619; + + expect(Number(userAccountXAfterSwap.amount)).toEqual(expectedAmountReceived);; + expect(Number(userAccountYAfterSwap.amount)).toEqual(fundAmountY - 1e8) + + const [poolTokenAccountX, poolTokenAccountY] = + await getOrCreateAssociatedTokenAccountsMulti( + provider.connection, + true, + payer, + poolPubkey, + [TOKEN_X, TOKEN_Y], + ); + + const poolAccountXAfterSwap = await getAccount( + provider.connection, + poolTokenAccountX.address, + undefined, + tokenXProgramId, + ); + const poolAccountYAfterSwap = await getAccount( + provider.connection, + poolTokenAccountY.address, + undefined, + tokenYProgramId, + ); + + expect(Number(poolAccountXAfterSwap.amount)).toEqual(initialLiquidityX - expectedAmountReceived); + expect(Number(poolAccountYAfterSwap.amount)).toEqual(initialLiquidityY + 1e8); + } catch (error) { + console.error('Error performing confidential swap:', error); + throw error; + } }, 10000000); }); - describe.skip('Remove liquidity', () => { + describe('Remove liquidity', () => { const amountX = 1_000_000; const amountY = 2_000_000_000; diff --git a/anchor/tests/jest/proof.ts b/anchor/tests/jest/proof.ts index bc20454..bf0f0b7 100644 --- a/anchor/tests/jest/proof.ts +++ b/anchor/tests/jest/proof.ts @@ -44,8 +44,6 @@ export async function generateProof( zkeyPath, ); - console.info("Sent publicSignals:", publicSignals) - const curve = await buildBn128(); const proofProc = unstringifyBigInts(proof); const publicSignalsUnstrigified = unstringifyBigInts(publicSignals);