From 42431c6b7c0aec55de83e1847912359908bd27e0 Mon Sep 17 00:00:00 2001 From: etsraphael Date: Thu, 19 Dec 2024 17:01:53 +1100 Subject: [PATCH 01/12] Feature: Add BNB network support with corresponding token pairs and addresses --- UI/src/app/shared/data/dex.data.ts | 5 +++++ UI/src/app/shared/interfaces/web3-tools.interface.ts | 2 ++ UI/src/environments/environment.development.ts | 8 ++++++++ UI/src/environments/environment.test.ts | 8 ++++++++ UI/src/environments/environment.ts | 8 ++++++++ 5 files changed, 31 insertions(+) diff --git a/UI/src/app/shared/data/dex.data.ts b/UI/src/app/shared/data/dex.data.ts index 870626c6..41463d69 100644 --- a/UI/src/app/shared/data/dex.data.ts +++ b/UI/src/app/shared/data/dex.data.ts @@ -74,6 +74,11 @@ export const DefaultNetworkPairs: DefaultNetworkPair[] = [ chainId: NetworkChainId.ETHEREUM, token1: findTokenById(TokenId.ETHEREUM) as IToken, token2: findTokenById('usdc') as IToken + }, + { + chainId: NetworkChainId.BNB, + token1: findTokenById(TokenId.BNB) as IToken, + token2: findTokenById('usdc') as IToken } ]; diff --git a/UI/src/app/shared/interfaces/web3-tools.interface.ts b/UI/src/app/shared/interfaces/web3-tools.interface.ts index a87213fe..42f7fa0a 100644 --- a/UI/src/app/shared/interfaces/web3-tools.interface.ts +++ b/UI/src/app/shared/interfaces/web3-tools.interface.ts @@ -13,3 +13,5 @@ export interface IReceiptTransaction { transactionIndex: number; type: bigint | undefined; } + +export const ZeroAddress: string = "0x0000000000000000000000000000000000000000"; diff --git a/UI/src/environments/environment.development.ts b/UI/src/environments/environment.development.ts index 7185bc86..44aafdbc 100644 --- a/UI/src/environments/environment.development.ts +++ b/UI/src/environments/environment.development.ts @@ -140,6 +140,10 @@ export const environment = { { chainId: NetworkChainId.POLYGON, address: '0x534B8c133D2ba1F02Aa8De539b177A989A209d0C' + }, + { + chainId: NetworkChainId.BNB, + address: '0x08e3a202e2b9e0bB5496Fc78579eDD7B39B0a9A4' } ] }, @@ -156,6 +160,10 @@ export const environment = { { chainId: NetworkChainId.POLYGON, address: '0x9839e975d9ab1b18f9708DeBAc5bfCD75Cff2684' + }, + { + chainId: NetworkChainId.BNB, + address: '0x32CBCF3fbca572B7fc6Bb2629B5daBC02B671F6B' } ] } diff --git a/UI/src/environments/environment.test.ts b/UI/src/environments/environment.test.ts index 910fe560..300f7412 100644 --- a/UI/src/environments/environment.test.ts +++ b/UI/src/environments/environment.test.ts @@ -132,6 +132,10 @@ export const environment = { { chainId: NetworkChainId.POLYGON, address: '0x534B8c133D2ba1F02Aa8De539b177A989A209d0C' + }, + { + chainId: NetworkChainId.BNB, + address: '0x08e3a202e2b9e0bB5496Fc78579eDD7B39B0a9A4' } ] }, @@ -148,6 +152,10 @@ export const environment = { { chainId: NetworkChainId.POLYGON, address: '0x9839e975d9ab1b18f9708DeBAc5bfCD75Cff2684' + }, + { + chainId: NetworkChainId.BNB, + address: '0x32CBCF3fbca572B7fc6Bb2629B5daBC02B671F6B' } ] } diff --git a/UI/src/environments/environment.ts b/UI/src/environments/environment.ts index 1d463ede..864ba2d0 100644 --- a/UI/src/environments/environment.ts +++ b/UI/src/environments/environment.ts @@ -128,6 +128,10 @@ export const environment = { { chainId: NetworkChainId.POLYGON, address: '0x534B8c133D2ba1F02Aa8De539b177A989A209d0C' + }, + { + chainId: NetworkChainId.BNB, + address: '0x08e3a202e2b9e0bB5496Fc78579eDD7B39B0a9A4' } ] }, @@ -144,6 +148,10 @@ export const environment = { { chainId: NetworkChainId.POLYGON, address: '0x9839e975d9ab1b18f9708DeBAc5bfCD75Cff2684' + }, + { + chainId: NetworkChainId.BNB, + address: '0x32CBCF3fbca572B7fc6Bb2629B5daBC02B671F6B' } ] } From 87df793ec44f6a23b3dc76e898d09475e38715d7 Mon Sep 17 00:00:00 2001 From: etsraphael Date: Thu, 19 Dec 2024 19:00:12 +1100 Subject: [PATCH 02/12] Feature: Enhance pool creation logic to support native tokens and improve zero address handling --- .../contracts/dex/ChainbrarySwapFactory.sol | 3 +- SmartContracts/test/ChainbrarySwapFactory.ts | 18 ++++++++--- .../dex-liquidity-page.component.ts | 32 +++++++++++-------- .../tokens-dialog/tokens-dialog.component.ts | 10 ++++-- UI/src/app/shared/data/dex.data.ts | 14 +++----- .../shared/interfaces/web3-tools.interface.ts | 2 +- UI/src/app/shared/services/dex/dex.service.ts | 20 ++++++------ 7 files changed, 57 insertions(+), 42 deletions(-) diff --git a/SmartContracts/contracts/dex/ChainbrarySwapFactory.sol b/SmartContracts/contracts/dex/ChainbrarySwapFactory.sol index 22bde0cb..be17282b 100644 --- a/SmartContracts/contracts/dex/ChainbrarySwapFactory.sol +++ b/SmartContracts/contracts/dex/ChainbrarySwapFactory.sol @@ -19,8 +19,9 @@ contract ChainbrarySwapFactory is Ownable, Initializable { function createPool(address tokenA, address tokenB, uint24 fee) external returns (address pool) { require(tokenA != tokenB, "Identical tokens"); - require(tokenA != address(0) && tokenB != address(0), "Zero address"); + require(tokenA != address(0) || tokenB != address(0), "Zero address"); require(getPool[tokenA][tokenB][fee] == address(0), "Pool exists"); + // Deploy the Pool contract Pool newPool = new Pool(); diff --git a/SmartContracts/test/ChainbrarySwapFactory.ts b/SmartContracts/test/ChainbrarySwapFactory.ts index 1550076b..e1ce64c8 100644 --- a/SmartContracts/test/ChainbrarySwapFactory.ts +++ b/SmartContracts/test/ChainbrarySwapFactory.ts @@ -40,7 +40,7 @@ describe('ChainbrarySwapFactory', function () { await createPoolTx.wait(); const poolAddress: string = await chainbrarySwapFactoryInstance.getPool(TOKEN_1_ADDRESS, TOKEN_2_ADDRESS, FEE_TIER_1); - expect(poolAddress).to.properAddress; + expect(poolAddress).to.be.properAddress; // Verify that the pool has been created and stored in the mapping for both token pairs expect(await chainbrarySwapFactoryInstance.getPool(TOKEN_1_ADDRESS, TOKEN_2_ADDRESS, FEE_TIER_1)).to.equal(poolAddress); @@ -66,10 +66,18 @@ describe('ChainbrarySwapFactory', function () { .to.be.revertedWith('Pool exists'); }); - it('should fail to create a pool if one of the token addresses is zero', async () => { + it('should create a pool with a native token', async () => { const { chainbrarySwapFactoryInstance, owner } = await loadFixture(deployChainbrarySwapFactoryFixture); - await expect(chainbrarySwapFactoryInstance.connect(owner).createPool(ethers.ZeroAddress, TOKEN_2_ADDRESS, FEE_TIER_1)) - .to.be.revertedWith('Zero address'); - }); + // Create pool for TOKEN_1_ADDRESS and ETH with FEE_TIER_1 + const createPoolTx: ContractTransactionResponse = await chainbrarySwapFactoryInstance.connect(owner).createPool(TOKEN_1_ADDRESS, ethers.ZeroAddress, FEE_TIER_1); + await createPoolTx.wait(); + + const poolAddress: string = await chainbrarySwapFactoryInstance.getPool(TOKEN_1_ADDRESS, ethers.ZeroAddress, FEE_TIER_1); + expect(poolAddress).to.properAddress; + + // Verify that the pool has been created and stored in the mapping for both token pairs + expect(await chainbrarySwapFactoryInstance.getPool(TOKEN_1_ADDRESS, ethers.ZeroAddress, FEE_TIER_1)).to.equal(poolAddress); + expect(await chainbrarySwapFactoryInstance.getPool(ethers.ZeroAddress, TOKEN_1_ADDRESS, FEE_TIER_1)).to.equal(poolAddress); + }) }); diff --git a/UI/src/app/page/use-cases-page/pages/dex/containers/dex-liquidity-page/dex-liquidity-page.component.ts b/UI/src/app/page/use-cases-page/pages/dex/containers/dex-liquidity-page/dex-liquidity-page.component.ts index 740c962c..0b9d6297 100644 --- a/UI/src/app/page/use-cases-page/pages/dex/containers/dex-liquidity-page/dex-liquidity-page.component.ts +++ b/UI/src/app/page/use-cases-page/pages/dex/containers/dex-liquidity-page/dex-liquidity-page.component.ts @@ -35,7 +35,8 @@ import { IPoolSearch, IToken, ITransactionCard, - StoreState + StoreState, + ZeroAddress } from '../../../../../../shared/interfaces'; import { selectWalletConnected } from '../../../../../../store/global-store/state/selectors'; import { @@ -251,12 +252,12 @@ export class DexLiquidityPageComponent implements OnInit, OnDestroy { createPool(): void { const payload: IPoolSearch = { - token1Address: this.tokenPath[0].networkSupport.find( - (tokenContract) => tokenContract.chainId === this.networkSelected.chainId - )?.address as string, - token2Address: this.tokenPath[1].networkSupport.find( - (tokenContract) => tokenContract.chainId === this.networkSelected.chainId - )?.address as string, + token1Address: + this.tokenPath[0].networkSupport.find((tokenContract) => tokenContract.chainId === this.networkSelected.chainId) + ?.address || ZeroAddress, + token2Address: + this.tokenPath[1].networkSupport.find((tokenContract) => tokenContract.chainId === this.networkSelected.chainId) + ?.address || ZeroAddress, chainId: this.networkSelected.chainId }; return this.store.dispatch(createPoolAction({ payload })); @@ -291,12 +292,12 @@ export class DexLiquidityPageComponent implements OnInit, OnDestroy { private loadPool(): void { const payload: IPoolSearch = { - token1Address: this.tokenPath[0].networkSupport.find( - (tokenContract) => tokenContract.chainId === this.networkSelected.chainId - )?.address as string, - token2Address: this.tokenPath[1].networkSupport.find( - (tokenContract) => tokenContract.chainId === this.networkSelected.chainId - )?.address as string, + token1Address: + this.tokenPath[0].networkSupport.find((tokenContract) => tokenContract.chainId === this.networkSelected.chainId) + ?.address || ZeroAddress, + token2Address: + this.tokenPath[1].networkSupport.find((tokenContract) => tokenContract.chainId === this.networkSelected.chainId) + ?.address || ZeroAddress, chainId: this.networkSelected.chainId }; return this.store.dispatch(loadPoolAction({ payload })); @@ -345,6 +346,11 @@ export class DexLiquidityPageComponent implements OnInit, OnDestroy { const token2: string | null = this.route.snapshot.queryParamMap.get('token2'); const chainId: string | null = this.route.snapshot.queryParamMap.get('chainId'); + if (chainId && !token1 && !token2) { + this.networkSelected = this.web3loginService.getNetworkDetailByChainId(chainId); + this.setUpDefaultNetworkPair(chainId as NetworkChainId); + } + if (!token1 || !token2 || !chainId) { // Set the default tokenPath this.handleTokenSelected(this.tokenPath[0], true, true); diff --git a/UI/src/app/shared/components/modal/tokens-dialog/tokens-dialog.component.ts b/UI/src/app/shared/components/modal/tokens-dialog/tokens-dialog.component.ts index d56942f3..6ec75b37 100644 --- a/UI/src/app/shared/components/modal/tokens-dialog/tokens-dialog.component.ts +++ b/UI/src/app/shared/components/modal/tokens-dialog/tokens-dialog.component.ts @@ -51,9 +51,13 @@ export class TokensDialogComponent implements OnInit, OnDestroy { token.name.toLowerCase().includes(this.searchTerm.toLowerCase()) ); if (this.data.chainIdSelected !== null) { - this.filteredTokens = this.filteredTokens.filter((token: IToken) => - token.networkSupport.some((network) => network.chainId === this.data.chainIdSelected) - ); + this.filteredTokens = [ + // add native token to the list + ...this.filteredTokens.filter((token: IToken) => token.nativeToChainId === this.data.chainIdSelected), + ...this.filteredTokens.filter((token: IToken) => + token.networkSupport.some((network) => network.chainId === this.data.chainIdSelected) + ) + ]; } } diff --git a/UI/src/app/shared/data/dex.data.ts b/UI/src/app/shared/data/dex.data.ts index 41463d69..d56959be 100644 --- a/UI/src/app/shared/data/dex.data.ts +++ b/UI/src/app/shared/data/dex.data.ts @@ -10,17 +10,11 @@ export const DefaultNetworkPairs: DefaultNetworkPair[] = [ { chainId: NetworkChainId.LOCALHOST, token1: { - tokenId: '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9', + tokenId: NetworkChainId.ETHEREUM, decimals: 18, - name: 'Custom Token 1', - symbol: 'CT1', - networkSupport: [ - { - chainId: NetworkChainId.LOCALHOST, - address: '0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9', - priceFeed: [] - } - ] + name: 'Ethereum', + symbol: 'ETH', + networkSupport: [] }, token2: { tokenId: '0x5FC8d32690cc91D4c39d9d3abcBD16989F875707', diff --git a/UI/src/app/shared/interfaces/web3-tools.interface.ts b/UI/src/app/shared/interfaces/web3-tools.interface.ts index 42f7fa0a..23202b06 100644 --- a/UI/src/app/shared/interfaces/web3-tools.interface.ts +++ b/UI/src/app/shared/interfaces/web3-tools.interface.ts @@ -14,4 +14,4 @@ export interface IReceiptTransaction { type: bigint | undefined; } -export const ZeroAddress: string = "0x0000000000000000000000000000000000000000"; +export const ZeroAddress: string = '0x0000000000000000000000000000000000000000'; diff --git a/UI/src/app/shared/services/dex/dex.service.ts b/UI/src/app/shared/services/dex/dex.service.ts index 85d97ba2..14340a55 100644 --- a/UI/src/app/shared/services/dex/dex.service.ts +++ b/UI/src/app/shared/services/dex/dex.service.ts @@ -9,7 +9,7 @@ import { SwapRouterObjectResponse } from '../../contracts'; import { SwapFactoryContract } from '../../contracts/swapFactory'; -import { IQuoteResult, IToken, ITokenContract } from '../../interfaces'; +import { IQuoteResult, IToken, ITokenContract, ZeroAddress } from '../../interfaces'; import { ILiquidityBalanceCheckPayload, ILiquidityPayload, @@ -113,12 +113,12 @@ export class DexService { async addLiquidity(from: string, payload: ILiquidityPayload): Promise { const web3: Web3 = new Web3(window.ethereum); - const address1: string = payload.token1.networkSupport.find( - (network: ITokenContract) => network.chainId === payload.chainId - )?.address as string; - const address2: string = payload.token2.networkSupport.find( - (network: ITokenContract) => network.chainId === payload.chainId - )?.address as string; + const address1: string = + payload.token1.networkSupport.find((network: ITokenContract) => network.chainId === payload.chainId)?.address || + ZeroAddress; + const address2: string = + payload.token2.networkSupport.find((network: ITokenContract) => network.chainId === payload.chainId)?.address || + ZeroAddress; return this.getPool({ chainId: payload.chainId, @@ -136,13 +136,15 @@ export class DexService { const amount1 = web3.utils.toWei(payload.token2Amount, 'ether'); const gasEstimate: bigint = await poolFragment.methods['addLiquidity'](amount0, amount1).estimateGas({ - from + from, + value: address1 === ZeroAddress ? amount0 : address2 === ZeroAddress ? amount1 : '0' }); return poolFragment.methods['addLiquidity'](amount0, amount1) .send({ from: from, - gas: gasEstimate.toString() + gas: gasEstimate.toString(), + value: address1 === ZeroAddress ? amount0 : address2 === ZeroAddress ? amount1 : '0' }) .then((receipt) => receipt.transactionHash); }) From 0ad23b96ffa6572ebd3d1dc27625882b1b136601 Mon Sep 17 00:00:00 2001 From: etsraphael Date: Thu, 19 Dec 2024 19:20:27 +1100 Subject: [PATCH 03/12] Feature: Update token handling to support nullable token addresses and add native balance loading logic --- .../dex-liquidity-page.component.ts | 6 +-- .../app/shared/interfaces/token.interface.ts | 2 +- .../shared/services/tokens/tokens.service.ts | 2 +- UI/src/app/store/swap-store/state/effects.ts | 38 ++++++++++++++++++- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/UI/src/app/page/use-cases-page/pages/dex/containers/dex-liquidity-page/dex-liquidity-page.component.ts b/UI/src/app/page/use-cases-page/pages/dex/containers/dex-liquidity-page/dex-liquidity-page.component.ts index 0b9d6297..854c33d9 100644 --- a/UI/src/app/page/use-cases-page/pages/dex/containers/dex-liquidity-page/dex-liquidity-page.component.ts +++ b/UI/src/app/page/use-cases-page/pages/dex/containers/dex-liquidity-page/dex-liquidity-page.component.ts @@ -305,9 +305,9 @@ export class DexLiquidityPageComponent implements OnInit, OnDestroy { private handleTokenSelected(token: IToken, tokenIn: boolean, skipRouteConfig: boolean): void { tokenIn ? (this.tokenPath[0] = token) : (this.tokenPath[1] = token); - const tokenAddress: string = token.networkSupport.find( - (tokenContract) => tokenContract.chainId === this.networkSelected.chainId - )?.address as string; + const tokenAddress: string | null = + token.networkSupport.find((tokenContract) => tokenContract.chainId === this.networkSelected.chainId)?.address ?? + null; !skipRouteConfig ? this.router.navigate([], { diff --git a/UI/src/app/shared/interfaces/token.interface.ts b/UI/src/app/shared/interfaces/token.interface.ts index 1fd2604d..4ba88e72 100644 --- a/UI/src/app/shared/interfaces/token.interface.ts +++ b/UI/src/app/shared/interfaces/token.interface.ts @@ -43,7 +43,7 @@ export interface IBalanceAndAllowancePayload { chainId: NetworkChainId; tokenId: TokenId | string; spender: string; - tokenAddress: string; + tokenAddress: string | null; tokenIn: boolean; } diff --git a/UI/src/app/shared/services/tokens/tokens.service.ts b/UI/src/app/shared/services/tokens/tokens.service.ts index fc891843..8f7e3e6a 100644 --- a/UI/src/app/shared/services/tokens/tokens.service.ts +++ b/UI/src/app/shared/services/tokens/tokens.service.ts @@ -231,7 +231,7 @@ export class TokensService { async getBalanceAndAllowance(from: string, payload: IBalanceAndAllowancePayload): Promise { const web3: Web3 = new Web3(this.web3ProviderService.getRpcUrl(payload.chainId)); - const erc20Contract = new ERC20TokenContract(payload.chainId, payload.tokenAddress); + const erc20Contract = new ERC20TokenContract(payload.chainId, payload.tokenAddress as string); const contractFragment: Contract = new web3.eth.Contract( erc20Contract.getAbi() as AbiItem[], diff --git a/UI/src/app/store/swap-store/state/effects.ts b/UI/src/app/store/swap-store/state/effects.ts index e8945051..57c7eab2 100644 --- a/UI/src/app/store/swap-store/state/effects.ts +++ b/UI/src/app/store/swap-store/state/effects.ts @@ -19,6 +19,7 @@ import { ITokenContract, StoreState } from '../../../shared/interfaces'; +import { AuthService } from '../../../shared/services/auth/auth.service'; import { DexService } from '../../../shared/services/dex/dex.service'; import { TokensService } from '../../../shared/services/tokens/tokens.service'; import { selectPublicAddress } from '../../auth-store/state/selectors'; @@ -34,7 +35,8 @@ export class SwapEffects { private web3LoginService: Web3LoginService, private readonly store: Store, private dexService: DexService, - private tokensService: TokensService + private tokensService: TokensService, + private authService: AuthService ) {} removeLiquidity$ = createEffect(() => { @@ -207,6 +209,40 @@ export class SwapEffects { ); }); + loadNativeBalance$ = createEffect(() => { + return this.actions$.pipe( + ofType(DexActions.loadBalanceAndAllowanceAction), + concatLatestFrom(() => [this.store.select(selectWalletConnected), this.store.select(selectPublicAddress)]), + map( + ( + payload: [ReturnType, WalletProvider | null, string | null] + ) => payload as [ReturnType, WalletProvider, string] + ), + filter( + (payload) => + payload[0]?.payload.tokenAddress === null && + payload[1] !== null && + payload[2] !== null && + !!this.authService.getRecentWallet() + ), + switchMap((action: [ReturnType, WalletProvider, string]) => { + const recentWallet: WalletProvider = this.authService.getRecentWallet() as WalletProvider; + return this.web3LoginService.getCurrentBalance(recentWallet).pipe( + map((response: number) => + DexActions.loadBalanceAndAllowanceActionSuccess({ + result: { + tokenId: action[0].payload.tokenId, + balance: response.toString(), + allowance: response.toString(), + tokenIn: action[0].payload.tokenIn + } + }) + ) + ); + }) + ); + }); + approveAllowance$ = createEffect(() => { return this.actions$.pipe( ofType(DexActions.approveAllowanceAction), From 22b6fae2e1c525663a1014c0646cb6148f2245f8 Mon Sep 17 00:00:00 2001 From: etsraphael Date: Fri, 20 Dec 2024 09:00:01 +1100 Subject: [PATCH 04/12] Feature: Improve pool creation logic to handle token address checks and add support for native tokens in the Dex interface --- .../contracts/dex/ChainbrarySwapFactory.sol | 6 +- SmartContracts/test/ChainbrarySwapRouter.ts | 118 ++++++++++++++++++ .../dex-swapping-page.component.ts | 25 ++-- UI/src/app/shared/services/dex/dex.service.ts | 9 +- 4 files changed, 144 insertions(+), 14 deletions(-) diff --git a/SmartContracts/contracts/dex/ChainbrarySwapFactory.sol b/SmartContracts/contracts/dex/ChainbrarySwapFactory.sol index be17282b..98911d16 100644 --- a/SmartContracts/contracts/dex/ChainbrarySwapFactory.sol +++ b/SmartContracts/contracts/dex/ChainbrarySwapFactory.sol @@ -20,8 +20,10 @@ contract ChainbrarySwapFactory is Ownable, Initializable { function createPool(address tokenA, address tokenB, uint24 fee) external returns (address pool) { require(tokenA != tokenB, "Identical tokens"); require(tokenA != address(0) || tokenB != address(0), "Zero address"); - require(getPool[tokenA][tokenB][fee] == address(0), "Pool exists"); - + require( + getPool[tokenA][tokenB][fee] == address(0) || getPool[tokenB][tokenA][fee] == address(0), + "Pool exists" + ); // Deploy the Pool contract Pool newPool = new Pool(); diff --git a/SmartContracts/test/ChainbrarySwapRouter.ts b/SmartContracts/test/ChainbrarySwapRouter.ts index 272ddda7..2a653fcd 100644 --- a/SmartContracts/test/ChainbrarySwapRouter.ts +++ b/SmartContracts/test/ChainbrarySwapRouter.ts @@ -114,6 +114,56 @@ describe('ChainbrarySwapRouter', function () { return { router, factory, poolAddress, tokenA, tokenB, owner, addr1, addr2 }; } + async function deployRouterWithNativeTokenFixture() { + const CustomERC20Token: CustomERC20Token__factory = await ethers.getContractFactory('CustomERC20Token'); + const ChainbrarySwapFactory: ChainbrarySwapFactory__factory = await ethers.getContractFactory('ChainbrarySwapFactory'); + const ChainbrarySwapRouter: ChainbrarySwapRouter__factory = await ethers.getContractFactory('ChainbrarySwapRouter'); + + const [owner, addr1, addr2] = await ethers.getSigners(); + + // Deploy token + const tokenA: CustomERC20Token = await CustomERC20Token.deploy( + owner.address, + 'CustomTokenA', + 'CTKA', + ethers.parseUnits('1000000', 'ether'), + true, + false, + false, + [], + [] + ); + + // Deploy factory and router + const factory: ChainbrarySwapFactory = await ChainbrarySwapFactory.deploy(); + const factoryAddress: string = await factory.getAddress(); + const router: ChainbrarySwapRouter = await ChainbrarySwapRouter.deploy(); + await router.initialize(factoryAddress, addr1.address); + + // Deploy a pool from the factory with native token + const tokenAAddress: string = await tokenA.getAddress(); + + // Listen for the PoolCreated event and create a pool + const tx: ContractTransactionResponse = await factory.createPool(tokenAAddress, ethers.ZeroAddress, FEE); + const receipt: ContractTransactionReceipt | null = await tx.wait(); + + if (!receipt) { + throw new Error('Transaction receipt not found'); + } + + // Extract the pool address from the event + const poolCreatedEvent: LogDescription | null | undefined = receipt.logs + .map((log: EventLog | Log) => factory.interface.parseLog(log)) + .find((event: LogDescription | null) => event?.name === 'PoolCreated'); + if (!poolCreatedEvent) { + throw new Error('PoolCreated event not found'); + } + + const poolAddress: string = poolCreatedEvent.args?.pool; + + return { router, factory, poolAddress, tokenA, owner, addr1, addr2 }; + } + it('should find the correct pool address', async () => { const { factory, tokenA, tokenB, poolAddress, addr1 } = await loadFixture(deployRouterFixture); @@ -245,6 +295,74 @@ describe('ChainbrarySwapRouter', function () { expect(balanceAfter).to.be.equal(expectedAmountOut); }); + // TODO: Fix this test + // it.only('should execute a token swap with a native token successfully', async () => { + + // const { router, factory, poolAddress, tokenA, addr1, addr2 } = await loadFixture(deployRouterWithNativeTokenFixture); + + // const tokenAAddress: string = await tokenA.getAddress(); + // const poolForAddr1: Pool = await getPoolFromPoolContract(poolAddress, addr1); + + // const path: string[] = [tokenAAddress, ethers.ZeroAddress]; + // const fees: number[] = [FEE]; + // const amountIn: bigint = SWAP_AMOUNT; // Increase the swap amount + // const amountOutMin: number = 1; // Set to 1 to ensure the output is greater than zero + // const liquidity: bigint[] = [INITIAL_LIQUIDITY_0, INITIAL_LIQUIDITY_1]; + + // const routerAddress: string = await router.getAddress(); + + // // Transfer tokens to addr1 and set approval and liquidity + // await tokenA.transfer(addr1.address, liquidity[0]); + // await tokenA.connect(addr1).approve(poolAddress, liquidity[0]); + + // // Add liquidity to the pool + // const addLiquidityTx = await poolForAddr1.connect(addr1).addLiquidity(liquidity[0], liquidity[1], { value: liquidity[0] }); + // await addLiquidityTx.wait(); + + // // Get reserves + // const reserve0BeforeSwap: bigint = await poolForAddr1.reserve0(); + // const reserve1BeforeSwap: bigint = await poolForAddr1.reserve1(); + // const fee: bigint = await poolForAddr1.fee(); + // expect(reserve0BeforeSwap).to.be.equal(BigInt(liquidity[0])); + // expect(reserve1BeforeSwap).to.be.equal(BigInt(0)); + // expect(fee).to.be.equal(BigInt(FEE)); + + // // Check if pool was created from factory + // expect(await factory.getPool(tokenAAddress, ethers.ZeroAddress, FEE)).to.equal(poolAddress); + + // // Set up addr2 with tokens + // await tokenA.transfer(addr2.address, amountIn); + // await tokenA.connect(addr2).approve(routerAddress, amountIn); + + // // Approve tokens for transfer + // await tokenA.connect(addr1).approve(poolAddress, amountIn); + + // // Add liquidity to the pool + // const addLiquidityTx2 = await poolForAddr1.connect(addr2).addLiquidity(amountIn, amountIn, { value: amountIn }); + // await addLiquidityTx2.wait(); + + // // Calculate the amountOut manually using the same logic as the contract + // const amountInWithFee: bigint = BigInt(amountIn) * BigInt(1000000 - FEE) / BigInt(1000000); + // const expectedAmountOut: bigint = (amountInWithFee * reserve0BeforeSwap) / (reserve1BeforeSwap + amountInWithFee); + + // // Get the output amounts from the router contract + // const routerInstance: ChainbrarySwapRouter = ChainbrarySwapRouter__factory.connect(routerAddress, addr2); + // const amountsOut: bigint[] = await routerInstance.getAmountsOut(amountIn, path, fees); + // const amountOut: bigint = amountsOut[1]; + + // // Compare manual calculation with the contract result + // expect(amountsOut[1]).to.be.equal(expectedAmountOut); + + // const balanceBefore: bigint = await ethers.provider.getBalance(addr2.address); + + // // Execute the swap if the above calculations are consistent + // await router.connect(addr2).swapExactTokensForTokens(amountIn, amountOutMin, path, fees, addr2.address); + + // // Check balances after swap + // const balanceAfter: bigint = await ethers.provider.getBalance(addr2.address); + // expect(balanceAfter).to.be.equal(balanceBefore + expectedAmountOut); + // }); + it('should fail to execute a swap if output is less than minimum specified', async () => { const { factory, router, tokenA, tokenB, addr1 } = await loadFixture(deployRouterFixture); diff --git a/UI/src/app/page/use-cases-page/pages/dex/containers/dex-swapping-page/dex-swapping-page.component.ts b/UI/src/app/page/use-cases-page/pages/dex/containers/dex-swapping-page/dex-swapping-page.component.ts index ef19a7e8..7adc1df9 100644 --- a/UI/src/app/page/use-cases-page/pages/dex/containers/dex-swapping-page/dex-swapping-page.component.ts +++ b/UI/src/app/page/use-cases-page/pages/dex/containers/dex-swapping-page/dex-swapping-page.component.ts @@ -38,7 +38,8 @@ import { ITokenContract, ITransactionCard, StoreState, - SwapPayload + SwapPayload, + ZeroAddress } from './../../../../../../shared/interfaces'; import { selectWalletConnected } from './../../../../../../store/global-store/state/selectors'; import { @@ -152,8 +153,8 @@ export class DexSwappingPageComponent implements OnInit, OnDestroy { ngOnInit(): void { this.setUpDefaultNetworkPair(this.networkPath[0].chainId); - this.loadPool(); this.fetchFormValues(); + this.loadPool(); this.listenFormChanges(); this.listenToQuote(); } @@ -207,10 +208,10 @@ export class DexSwappingPageComponent implements OnInit, OnDestroy { const token0Address: string = this.tokenPath[0].networkSupport.find( (network: ITokenContract) => network.chainId === this.networkPath[0].chainId - )?.address as string; + )?.address || ZeroAddress; const token1Address: string = this.tokenPath[1].networkSupport.find( (network: ITokenContract) => network.chainId === this.networkPath[1].chainId - )?.address as string; + )?.address || ZeroAddress; const fromAmount: string = (this.swapForm.get('fromAmount')?.value as number).toString(); const payload: ISwappingPayload = { @@ -249,10 +250,10 @@ export class DexSwappingPageComponent implements OnInit, OnDestroy { const payload: IPoolSearch = { token1Address: this.tokenPath[0].networkSupport.find( (tokenContract) => tokenContract.chainId === this.networkPath[0].chainId - )?.address as string, + )?.address || ZeroAddress, token2Address: this.tokenPath[1].networkSupport.find( (tokenContract) => tokenContract.chainId === this.networkPath[0].chainId - )?.address as string, + )?.address || ZeroAddress, chainId: this.networkPath[0].chainId }; return this.store.dispatch(loadPoolAction({ payload })); @@ -274,9 +275,9 @@ export class DexSwappingPageComponent implements OnInit, OnDestroy { private handleTokenSelected(token: IToken, tokenIn: boolean, skipRouteConfig: boolean): void { tokenIn ? (this.tokenPath[0] = token) : (this.tokenPath[1] = token); - const tokenAddress: string = token.networkSupport.find( + const tokenAddress: string | null = token.networkSupport.find( (network: ITokenContract) => network.chainId === this.networkPath[tokenIn ? 0 : 1].chainId - )?.address as string; + )?.address ?? null ; const payload: IBalanceAndAllowancePayload = { chainId: this.networkPath[0].chainId, @@ -325,6 +326,14 @@ export class DexSwappingPageComponent implements OnInit, OnDestroy { const token2: string | null = this.route.snapshot.queryParamMap.get('token2'); const chainIdIn: string | null = this.route.snapshot.queryParamMap.get('chainIdIn'); + if (chainIdIn && !token1 && !token2) { + this.networkPath = [ + this.web3loginService.getNetworkDetailByChainId(chainIdIn as NetworkChainId), + this.web3loginService.getNetworkDetailByChainId(chainIdIn as NetworkChainId) + ]; + this.setUpDefaultNetworkPair(chainIdIn as NetworkChainId); + } + if (!token1 || !token2 || !chainIdIn) { // Set the default tokenPath this.handleTokenSelected(this.tokenPath[0], true, true); diff --git a/UI/src/app/shared/services/dex/dex.service.ts b/UI/src/app/shared/services/dex/dex.service.ts index 14340a55..678f6963 100644 --- a/UI/src/app/shared/services/dex/dex.service.ts +++ b/UI/src/app/shared/services/dex/dex.service.ts @@ -69,6 +69,7 @@ export class DexService { ); } + // TODO: This should include the real amount and be called every time the form gets updated async getAmountsOut(payload: SwapPayload): Promise { const web3: Web3 = new Web3(this.web3ProviderService.getRpcUrl(payload.chainId)); const swapRouterContract = new SwapRouterContract(payload.chainId); @@ -78,12 +79,12 @@ export class DexService { swapRouterContract.getAddress() ); - const tokenInAddress: string | undefined = payload.from.networkSupport.find( + const tokenInAddress: string = payload.from.networkSupport.find( (network: ITokenContract) => network.chainId === payload.chainId - )?.address; - const tokenOutAddress: string | undefined = payload.to.networkSupport.find( + )?.address || ZeroAddress; + const tokenOutAddress: string = payload.to.networkSupport.find( (network: ITokenContract) => network.chainId === payload.chainId - )?.address; + )?.address || ZeroAddress; if (!tokenInAddress || !tokenOutAddress) { return Promise.reject('Token not supported on this network'); From 7ed8a2bbabf2a6564c1a016344d6315d9bb4254e Mon Sep 17 00:00:00 2001 From: etsraphael Date: Fri, 20 Dec 2024 17:14:41 +1100 Subject: [PATCH 05/12] Feature: Update ChainbrarySwapRouter tests to increase swap amount and enhance pool creation logic --- SmartContracts/test/ChainbrarySwapRouter.ts | 80 +++++++++++---------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/SmartContracts/test/ChainbrarySwapRouter.ts b/SmartContracts/test/ChainbrarySwapRouter.ts index 2a653fcd..e82fe5d3 100644 --- a/SmartContracts/test/ChainbrarySwapRouter.ts +++ b/SmartContracts/test/ChainbrarySwapRouter.ts @@ -17,7 +17,7 @@ import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; const FEE = 100; const INITIAL_LIQUIDITY_0: bigint = ethers.parseUnits('100000', 'ether'); const INITIAL_LIQUIDITY_1: bigint = ethers.parseUnits('100000', 'ether'); -const SWAP_AMOUNT: bigint = ethers.parseUnits('1', 'ether'); +const SWAP_AMOUNT: bigint = ethers.parseUnits('10', 'ether'); describe('ChainbrarySwapRouter', function () { @@ -144,7 +144,7 @@ describe('ChainbrarySwapRouter', function () { const tokenAAddress: string = await tokenA.getAddress(); // Listen for the PoolCreated event and create a pool - const tx: ContractTransactionResponse = await factory.createPool(tokenAAddress, ethers.ZeroAddress, FEE); + const tx: ContractTransactionResponse = await factory.createPool(ethers.ZeroAddress, tokenAAddress,FEE); const receipt: ContractTransactionReceipt | null = await tx.wait(); if (!receipt) { @@ -296,50 +296,58 @@ describe('ChainbrarySwapRouter', function () { }); // TODO: Fix this test - // it.only('should execute a token swap with a native token successfully', async () => { + it.only('should execute a token swap with a native token successfully', async () => { - // const { router, factory, poolAddress, tokenA, addr1, addr2 } = await loadFixture(deployRouterWithNativeTokenFixture); + const { router, factory, poolAddress, tokenA, addr1, addr2 } = await loadFixture(deployRouterWithNativeTokenFixture); - // const tokenAAddress: string = await tokenA.getAddress(); - // const poolForAddr1: Pool = await getPoolFromPoolContract(poolAddress, addr1); + const tokenAAddress: string = await tokenA.getAddress(); + const pool: Pool = await getPoolFromPoolContract(poolAddress, addr1); - // const path: string[] = [tokenAAddress, ethers.ZeroAddress]; - // const fees: number[] = [FEE]; - // const amountIn: bigint = SWAP_AMOUNT; // Increase the swap amount - // const amountOutMin: number = 1; // Set to 1 to ensure the output is greater than zero - // const liquidity: bigint[] = [INITIAL_LIQUIDITY_0, INITIAL_LIQUIDITY_1]; + const path: string[] = [tokenAAddress, ethers.ZeroAddress]; + const fees: number[] = [FEE]; + const amountIn: bigint = SWAP_AMOUNT; // Increase the swap amount + const amountOutMin: number = 1; // Set to 1 to ensure the output is greater than zero + const liquidity: bigint[] = [INITIAL_LIQUIDITY_0, INITIAL_LIQUIDITY_1]; - // const routerAddress: string = await router.getAddress(); + const routerAddress: string = await router.getAddress(); - // // Transfer tokens to addr1 and set approval and liquidity - // await tokenA.transfer(addr1.address, liquidity[0]); - // await tokenA.connect(addr1).approve(poolAddress, liquidity[0]); + // Transfer tokens to addr1 and set approval and liquidity + await tokenA.transfer(addr1.address, liquidity[0]); + await tokenA.connect(addr1).approve(poolAddress, liquidity[0]); - // // Add liquidity to the pool - // const addLiquidityTx = await poolForAddr1.connect(addr1).addLiquidity(liquidity[0], liquidity[1], { value: liquidity[0] }); - // await addLiquidityTx.wait(); + // Add liquidity to the pool + const addLiquidityTx = await pool.connect(addr1).addLiquidity(liquidity[0], liquidity[1], { value: liquidity[0] }); + await addLiquidityTx.wait(); - // // Get reserves - // const reserve0BeforeSwap: bigint = await poolForAddr1.reserve0(); - // const reserve1BeforeSwap: bigint = await poolForAddr1.reserve1(); - // const fee: bigint = await poolForAddr1.fee(); - // expect(reserve0BeforeSwap).to.be.equal(BigInt(liquidity[0])); - // expect(reserve1BeforeSwap).to.be.equal(BigInt(0)); - // expect(fee).to.be.equal(BigInt(FEE)); + // Get reserves + const reserve0BeforeSwap: bigint = await pool.reserve0(); + const reserve1BeforeSwap: bigint = await pool.reserve1(); + const fee: bigint = await pool.fee(); + expect(reserve0BeforeSwap).to.be.equal(BigInt(liquidity[0])); + expect(reserve1BeforeSwap).to.be.equal(BigInt(liquidity[0])); + expect(fee).to.be.equal(BigInt(FEE)); - // // Check if pool was created from factory - // expect(await factory.getPool(tokenAAddress, ethers.ZeroAddress, FEE)).to.equal(poolAddress); + // Check if pool was created from factory + expect(await factory.getPool(tokenAAddress, ethers.ZeroAddress, FEE)).to.equal(poolAddress); - // // Set up addr2 with tokens - // await tokenA.transfer(addr2.address, amountIn); - // await tokenA.connect(addr2).approve(routerAddress, amountIn); + // Swap from native token to ERC20 token + const balanceBefore: bigint = await tokenA.balanceOf(addr2.address); + expect(balanceBefore).to.be.equal(0); - // // Approve tokens for transfer - // await tokenA.connect(addr1).approve(poolAddress, amountIn); + // Check if the pool has the correct token addresses + const token0 = await pool.token0(); + const token1 = await pool.token1(); + expect(token0).to.be.equal(ethers.ZeroAddress); + expect(token1).to.be.equal(tokenAAddress); - // // Add liquidity to the pool - // const addLiquidityTx2 = await poolForAddr1.connect(addr2).addLiquidity(amountIn, amountIn, { value: amountIn }); - // await addLiquidityTx2.wait(); + // check balance of the pool + const poolBalance = await ethers.provider.getBalance(poolAddress); + expect(poolBalance).to.be.equal(liquidity[0]); + + // Execute the swap from pool + const halfAmountIn: bigint = amountIn / BigInt(2); + expect(halfAmountIn).to.be.gt(0); + // await pool.connect(addr2).swap(halfAmountIn, ethers.ZeroAddress, addr2.address, { value: halfAmountIn }); // // Calculate the amountOut manually using the same logic as the contract // const amountInWithFee: bigint = BigInt(amountIn) * BigInt(1000000 - FEE) / BigInt(1000000); @@ -361,7 +369,7 @@ describe('ChainbrarySwapRouter', function () { // // Check balances after swap // const balanceAfter: bigint = await ethers.provider.getBalance(addr2.address); // expect(balanceAfter).to.be.equal(balanceBefore + expectedAmountOut); - // }); + }); it('should fail to execute a swap if output is less than minimum specified', async () => { const { factory, router, tokenA, tokenB, addr1 } = await loadFixture(deployRouterFixture); From d494f7ead6b14725612283cfb2c795c7cf996ace Mon Sep 17 00:00:00 2001 From: etsraphael Date: Mon, 23 Dec 2024 12:51:36 +1100 Subject: [PATCH 06/12] save WIP --- .../contracts/dex/ChainbrarySwapFactory.sol | 8 +- SmartContracts/contracts/dex/Pool.sol | 8 +- SmartContracts/test/ChainbrarySwapRouter.ts | 79 +++++++++++-------- SmartContracts/test/Pool.ts | 2 +- UI/src/app/shared/services/dex/dex.service.ts | 6 +- 5 files changed, 65 insertions(+), 38 deletions(-) diff --git a/SmartContracts/contracts/dex/ChainbrarySwapFactory.sol b/SmartContracts/contracts/dex/ChainbrarySwapFactory.sol index 98911d16..fc880bd2 100644 --- a/SmartContracts/contracts/dex/ChainbrarySwapFactory.sol +++ b/SmartContracts/contracts/dex/ChainbrarySwapFactory.sol @@ -8,6 +8,7 @@ import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; contract ChainbrarySwapFactory is Ownable, Initializable { mapping(address => mapping(address => mapping(uint24 => address))) public getPool; uint24[] public feeTiers; + address public devAddress; event PoolCreated(address indexed tokenA, address indexed tokenB, uint24 fee, address pool); @@ -15,6 +16,7 @@ contract ChainbrarySwapFactory is Ownable, Initializable { function initialize() external initializer onlyOwner { feeTiers = [500, 3000, 10000]; + devAddress = _msgSender(); } function createPool(address tokenA, address tokenB, uint24 fee) external returns (address pool) { @@ -29,7 +31,7 @@ contract ChainbrarySwapFactory is Ownable, Initializable { Pool newPool = new Pool(); // Initialize the Pool contract - newPool.initialize(tokenA, tokenB, fee); + newPool.initialize(tokenA, tokenB, fee, _msgSender()); pool = address(newPool); getPool[tokenA][tokenB][fee] = pool; @@ -37,4 +39,8 @@ contract ChainbrarySwapFactory is Ownable, Initializable { emit PoolCreated(tokenA, tokenB, fee, pool); } + + function setDevAddress(address _devAddress) external onlyOwner { + devAddress = _devAddress; + } } diff --git a/SmartContracts/contracts/dex/Pool.sol b/SmartContracts/contracts/dex/Pool.sol index cd518b93..a1c97e52 100644 --- a/SmartContracts/contracts/dex/Pool.sol +++ b/SmartContracts/contracts/dex/Pool.sol @@ -13,6 +13,7 @@ contract Pool is Ownable, ReentrancyGuard, Initializable { address public token0; address public token1; uint24 public fee; // Fee in hundredths of a bip + address public devAddress; uint256 public reserve0; uint256 public reserve1; @@ -26,13 +27,14 @@ contract Pool is Ownable, ReentrancyGuard, Initializable { constructor() Ownable(_msgSender()) {} - function initialize(address _token0, address _token1, uint24 _fee) external initializer onlyOwner { + function initialize(address _token0, address _token1, uint24 _fee, address _devAddress) external initializer onlyOwner { require(_token0 != _token1, "Tokens must be different"); require(_fee > 0 && _fee < 1000000, "Invalid fee"); token0 = _token0; token1 = _token1; fee = _fee; + devAddress = _devAddress; } function addLiquidity(uint256 amount0, uint256 amount1) external payable nonReentrant { @@ -130,9 +132,9 @@ contract Pool is Ownable, ReentrancyGuard, Initializable { uint256 feeAmount = amountIn - amountInWithFee; // Transfer the fee to the contract owner if (tokenIn == address(0)) { - payable(owner()).transfer(feeAmount); + payable(devAddress).transfer(feeAmount); } else { - IERC20(tokenIn).safeTransfer(owner(), feeAmount); + IERC20(tokenIn).safeTransfer(devAddress, feeAmount); } // Update the reserves based on the input and output amounts diff --git a/SmartContracts/test/ChainbrarySwapRouter.ts b/SmartContracts/test/ChainbrarySwapRouter.ts index e82fe5d3..1b97103d 100644 --- a/SmartContracts/test/ChainbrarySwapRouter.ts +++ b/SmartContracts/test/ChainbrarySwapRouter.ts @@ -301,9 +301,10 @@ describe('ChainbrarySwapRouter', function () { const { router, factory, poolAddress, tokenA, addr1, addr2 } = await loadFixture(deployRouterWithNativeTokenFixture); const tokenAAddress: string = await tokenA.getAddress(); - const pool: Pool = await getPoolFromPoolContract(poolAddress, addr1); + const poolForAddr1: Pool = await getPoolFromPoolContract(poolAddress, addr1); + const poolForAddr2: Pool = await getPoolFromPoolContract(poolAddress, addr2); - const path: string[] = [tokenAAddress, ethers.ZeroAddress]; + const path: string[] = [ethers.ZeroAddress, tokenAAddress]; const fees: number[] = [FEE]; const amountIn: bigint = SWAP_AMOUNT; // Increase the swap amount const amountOutMin: number = 1; // Set to 1 to ensure the output is greater than zero @@ -315,16 +316,19 @@ describe('ChainbrarySwapRouter', function () { await tokenA.transfer(addr1.address, liquidity[0]); await tokenA.connect(addr1).approve(poolAddress, liquidity[0]); + // Add liquidity to the pool - const addLiquidityTx = await pool.connect(addr1).addLiquidity(liquidity[0], liquidity[1], { value: liquidity[0] }); + const addLiquidityTx = await poolForAddr1.connect(addr1).addLiquidity(liquidity[0], liquidity[1], { value: liquidity[0] }); await addLiquidityTx.wait(); + console.log('await poolForAddr1.reserve1()', (await poolForAddr1.reserve1()).toString()); + // Get reserves - const reserve0BeforeSwap: bigint = await pool.reserve0(); - const reserve1BeforeSwap: bigint = await pool.reserve1(); - const fee: bigint = await pool.fee(); + const reserve0BeforeSwap: bigint = await poolForAddr1.reserve0(); + const reserve1BeforeSwap: bigint = await poolForAddr1.reserve1(); + const fee: bigint = await poolForAddr1.fee(); expect(reserve0BeforeSwap).to.be.equal(BigInt(liquidity[0])); - expect(reserve1BeforeSwap).to.be.equal(BigInt(liquidity[0])); + expect(reserve1BeforeSwap).to.be.equal(BigInt(liquidity[1])); expect(fee).to.be.equal(BigInt(FEE)); // Check if pool was created from factory @@ -335,8 +339,8 @@ describe('ChainbrarySwapRouter', function () { expect(balanceBefore).to.be.equal(0); // Check if the pool has the correct token addresses - const token0 = await pool.token0(); - const token1 = await pool.token1(); + const token0 = await poolForAddr2.token0(); + const token1 = await poolForAddr2.token1(); expect(token0).to.be.equal(ethers.ZeroAddress); expect(token1).to.be.equal(tokenAAddress); @@ -347,28 +351,41 @@ describe('ChainbrarySwapRouter', function () { // Execute the swap from pool const halfAmountIn: bigint = amountIn / BigInt(2); expect(halfAmountIn).to.be.gt(0); - // await pool.connect(addr2).swap(halfAmountIn, ethers.ZeroAddress, addr2.address, { value: halfAmountIn }); - - // // Calculate the amountOut manually using the same logic as the contract - // const amountInWithFee: bigint = BigInt(amountIn) * BigInt(1000000 - FEE) / BigInt(1000000); - // const expectedAmountOut: bigint = (amountInWithFee * reserve0BeforeSwap) / (reserve1BeforeSwap + amountInWithFee); - - // // Get the output amounts from the router contract - // const routerInstance: ChainbrarySwapRouter = ChainbrarySwapRouter__factory.connect(routerAddress, addr2); - // const amountsOut: bigint[] = await routerInstance.getAmountsOut(amountIn, path, fees); - // const amountOut: bigint = amountsOut[1]; - - // // Compare manual calculation with the contract result - // expect(amountsOut[1]).to.be.equal(expectedAmountOut); - - // const balanceBefore: bigint = await ethers.provider.getBalance(addr2.address); - - // // Execute the swap if the above calculations are consistent - // await router.connect(addr2).swapExactTokensForTokens(amountIn, amountOutMin, path, fees, addr2.address); - - // // Check balances after swap - // const balanceAfter: bigint = await ethers.provider.getBalance(addr2.address); - // expect(balanceAfter).to.be.equal(balanceBefore + expectedAmountOut); + await poolForAddr2.connect(addr2).swap(halfAmountIn, ethers.ZeroAddress, addr2.address, { value: halfAmountIn }); + + // Calculate the amountOut manually using the same logic as the contract + const amountInWithFee: bigint = BigInt(halfAmountIn) * BigInt(1000000 - FEE); + const reserveAndCurrentFeeAdded: bigint = reserve1BeforeSwap + BigInt(halfAmountIn); + const numerator: bigint = amountInWithFee * reserveAndCurrentFeeAdded; + const denominator: bigint = (reserve0BeforeSwap * BigInt(1000000)) + (BigInt(amountInWithFee)); + const expectedAmountOut: bigint = (numerator * BigInt(1e18) / denominator) / BigInt(1e18); + + console.log('amountInWithFee:', amountInWithFee.toString()); + console.log('Fee:', FEE); + console.log('Half Amount In:', halfAmountIn.toString()); + console.log('Reserve0 Before:', reserve0BeforeSwap.toString()); + console.log('reserveOut:', reserve1BeforeSwap.toString()); + console.log('Expected Amount Out:', expectedAmountOut.toString()); + console.log('numerator:', numerator.toString()); + console.log('await poolForAddr1.reserve1()', (await poolForAddr1.reserve1()).toString()); + console.log('reserveAndCurrentFeeAdded', reserveAndCurrentFeeAdded.toString()); + + // Get the output amounts from the router contract + const routerInstance: ChainbrarySwapRouter = ChainbrarySwapRouter__factory.connect(routerAddress, addr2); + const amountsOut: bigint[] = await routerInstance.getAmountsOut(halfAmountIn, path, fees); + const amountOut: bigint = amountsOut[1]; + + // Compare manual calculation with the contract result + expect(amountsOut[1]).to.be.equal(amountOut); + + const balanceBefore1: bigint = await ethers.provider.getBalance(addr2.address); + + // Execute the swap if the above calculations are consistent + await router.connect(addr2).swapExactTokensForTokens(halfAmountIn, amountOutMin, path, fees, addr2.address, { value: halfAmountIn }); + + // Check balances after swap + const balanceAfter: bigint = await ethers.provider.getBalance(addr2.address); + // expect(balanceAfter).to.be.equal(balanceBefore1 + expectedAmountOut); }); it('should fail to execute a swap if output is less than minimum specified', async () => { diff --git a/SmartContracts/test/Pool.ts b/SmartContracts/test/Pool.ts index ff43fdc3..42f93d60 100644 --- a/SmartContracts/test/Pool.ts +++ b/SmartContracts/test/Pool.ts @@ -50,7 +50,7 @@ describe('Pool', function () { const pool: Pool__factory = await ethers.getContractFactory('Pool'); const [owner, addr1, addr2] = await ethers.getSigners(); const poolInstance: Pool = await pool.connect(owner).deploy(); - await poolInstance.initialize(tokenAAddress, tokenBAddress, FEE); + await poolInstance.initialize(tokenAAddress, tokenBAddress, FEE, owner.address); return { poolInstance, owner, addr1, addr2 }; }; diff --git a/UI/src/app/shared/services/dex/dex.service.ts b/UI/src/app/shared/services/dex/dex.service.ts index 678f6963..543e9c6b 100644 --- a/UI/src/app/shared/services/dex/dex.service.ts +++ b/UI/src/app/shared/services/dex/dex.service.ts @@ -273,7 +273,8 @@ export class DexService { from ) .estimateGas({ - from: from + from: from, + value: tokenInAddress === ZeroAddress ? web3.utils.toWei(payload.amount, 'ether') : tokenOutAddress === ZeroAddress ? web3.utils.toWei(payload.amount, 'ether') : '0' }) .catch((error: string) => { return Promise.reject(error); @@ -288,7 +289,8 @@ export class DexService { ) .send({ from: from, - gas: gasEstimate.toString() + gas: gasEstimate.toString(), + value: tokenInAddress === ZeroAddress ? web3.utils.toWei(payload.amount, 'ether') : tokenOutAddress === ZeroAddress ? web3.utils.toWei(payload.amount, 'ether') : '0' }) .then((receipt) => receipt.transactionHash) .catch((error: string) => Promise.reject(error)); From 08948f0fd522e73fac779bd0b41811cdc4afc865 Mon Sep 17 00:00:00 2001 From: etsraphael Date: Mon, 23 Dec 2024 15:02:45 +1100 Subject: [PATCH 07/12] Feature: Refactor Pool contract initialization and enhance ChainbrarySwapRouter tests for native token swaps --- SmartContracts/contracts/dex/Pool.sol | 9 ++++-- SmartContracts/test/ChainbrarySwapRouter.ts | 33 ++++++--------------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/SmartContracts/contracts/dex/Pool.sol b/SmartContracts/contracts/dex/Pool.sol index a1c97e52..9b9b43aa 100644 --- a/SmartContracts/contracts/dex/Pool.sol +++ b/SmartContracts/contracts/dex/Pool.sol @@ -27,7 +27,12 @@ contract Pool is Ownable, ReentrancyGuard, Initializable { constructor() Ownable(_msgSender()) {} - function initialize(address _token0, address _token1, uint24 _fee, address _devAddress) external initializer onlyOwner { + function initialize( + address _token0, + address _token1, + uint24 _fee, + address _devAddress + ) external initializer onlyOwner { require(_token0 != _token1, "Tokens must be different"); require(_fee > 0 && _fee < 1000000, "Invalid fee"); @@ -132,7 +137,7 @@ contract Pool is Ownable, ReentrancyGuard, Initializable { uint256 feeAmount = amountIn - amountInWithFee; // Transfer the fee to the contract owner if (tokenIn == address(0)) { - payable(devAddress).transfer(feeAmount); + payable(devAddress).transfer(feeAmount); } else { IERC20(tokenIn).safeTransfer(devAddress, feeAmount); } diff --git a/SmartContracts/test/ChainbrarySwapRouter.ts b/SmartContracts/test/ChainbrarySwapRouter.ts index 1b97103d..1e7e4914 100644 --- a/SmartContracts/test/ChainbrarySwapRouter.ts +++ b/SmartContracts/test/ChainbrarySwapRouter.ts @@ -13,6 +13,7 @@ import { Pool__factory } from '../typechain-types'; import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import BigNumber from 'bignumber.js'; const FEE = 100; const INITIAL_LIQUIDITY_0: bigint = ethers.parseUnits('100000', 'ether'); @@ -295,8 +296,7 @@ describe('ChainbrarySwapRouter', function () { expect(balanceAfter).to.be.equal(expectedAmountOut); }); - // TODO: Fix this test - it.only('should execute a token swap with a native token successfully', async () => { + it('should execute a token swap with a native token successfully', async () => { const { router, factory, poolAddress, tokenA, addr1, addr2 } = await loadFixture(deployRouterWithNativeTokenFixture); @@ -321,8 +321,6 @@ describe('ChainbrarySwapRouter', function () { const addLiquidityTx = await poolForAddr1.connect(addr1).addLiquidity(liquidity[0], liquidity[1], { value: liquidity[0] }); await addLiquidityTx.wait(); - console.log('await poolForAddr1.reserve1()', (await poolForAddr1.reserve1()).toString()); - // Get reserves const reserve0BeforeSwap: bigint = await poolForAddr1.reserve0(); const reserve1BeforeSwap: bigint = await poolForAddr1.reserve1(); @@ -351,41 +349,28 @@ describe('ChainbrarySwapRouter', function () { // Execute the swap from pool const halfAmountIn: bigint = amountIn / BigInt(2); expect(halfAmountIn).to.be.gt(0); - await poolForAddr2.connect(addr2).swap(halfAmountIn, ethers.ZeroAddress, addr2.address, { value: halfAmountIn }); // Calculate the amountOut manually using the same logic as the contract - const amountInWithFee: bigint = BigInt(halfAmountIn) * BigInt(1000000 - FEE); - const reserveAndCurrentFeeAdded: bigint = reserve1BeforeSwap + BigInt(halfAmountIn); - const numerator: bigint = amountInWithFee * reserveAndCurrentFeeAdded; - const denominator: bigint = (reserve0BeforeSwap * BigInt(1000000)) + (BigInt(amountInWithFee)); - const expectedAmountOut: bigint = (numerator * BigInt(1e18) / denominator) / BigInt(1e18); + const amountInWithFee: bigint = BigInt(halfAmountIn) * BigInt(1000000 - FEE) / BigInt(1000000); + const expectedAmountOut: bigint = (amountInWithFee * reserve1BeforeSwap) / (reserve0BeforeSwap + amountInWithFee); - console.log('amountInWithFee:', amountInWithFee.toString()); - console.log('Fee:', FEE); - console.log('Half Amount In:', halfAmountIn.toString()); - console.log('Reserve0 Before:', reserve0BeforeSwap.toString()); - console.log('reserveOut:', reserve1BeforeSwap.toString()); - console.log('Expected Amount Out:', expectedAmountOut.toString()); - console.log('numerator:', numerator.toString()); - console.log('await poolForAddr1.reserve1()', (await poolForAddr1.reserve1()).toString()); - console.log('reserveAndCurrentFeeAdded', reserveAndCurrentFeeAdded.toString()); - // Get the output amounts from the router contract const routerInstance: ChainbrarySwapRouter = ChainbrarySwapRouter__factory.connect(routerAddress, addr2); const amountsOut: bigint[] = await routerInstance.getAmountsOut(halfAmountIn, path, fees); const amountOut: bigint = amountsOut[1]; // Compare manual calculation with the contract result - expect(amountsOut[1]).to.be.equal(amountOut); + expect(expectedAmountOut).to.be.equal(amountOut); - const balanceBefore1: bigint = await ethers.provider.getBalance(addr2.address); + const balanceOfERC20TokenBefore: bigint = await tokenA.balanceOf(addr2.address); // Execute the swap if the above calculations are consistent await router.connect(addr2).swapExactTokensForTokens(halfAmountIn, amountOutMin, path, fees, addr2.address, { value: halfAmountIn }); // Check balances after swap - const balanceAfter: bigint = await ethers.provider.getBalance(addr2.address); - // expect(balanceAfter).to.be.equal(balanceBefore1 + expectedAmountOut); + const balanceOfERC20TokenAfter: bigint = await tokenA.balanceOf(addr2.address); + + expect(balanceOfERC20TokenAfter).to.be.equal(expectedAmountOut + balanceOfERC20TokenBefore); }); it('should fail to execute a swap if output is less than minimum specified', async () => { From b4ce68acbb20a73b57b44c96b926b54143e1b642 Mon Sep 17 00:00:00 2001 From: etsraphael Date: Mon, 23 Dec 2024 16:00:33 +1100 Subject: [PATCH 08/12] Feature: Enhance ChainbrarySwapRouter tests for improved token handling and native balance loading --- SmartContracts/test/ChainbrarySwapRouter.ts | 193 +++++++++++--------- 1 file changed, 102 insertions(+), 91 deletions(-) diff --git a/SmartContracts/test/ChainbrarySwapRouter.ts b/SmartContracts/test/ChainbrarySwapRouter.ts index 1e7e4914..57060162 100644 --- a/SmartContracts/test/ChainbrarySwapRouter.ts +++ b/SmartContracts/test/ChainbrarySwapRouter.ts @@ -12,7 +12,7 @@ import { Pool, Pool__factory } from '../typechain-types'; -import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers'; import BigNumber from 'bignumber.js'; const FEE = 100; @@ -21,19 +21,29 @@ const INITIAL_LIQUIDITY_1: bigint = ethers.parseUnits('100000', 'ether'); const SWAP_AMOUNT: bigint = ethers.parseUnits('10', 'ether'); describe('ChainbrarySwapRouter', function () { - - const getPoolFromFactory = async (factory: ChainbrarySwapFactory, tokenA: CustomERC20Token, tokenB: CustomERC20Token, addr: HardhatEthersSigner): Promise => { + const getPoolFromFactory = async ( + factory: ChainbrarySwapFactory, + tokenA: CustomERC20Token, + tokenB: CustomERC20Token, + addr: HardhatEthersSigner + ): Promise => { const tokenAAddress: string = await tokenA.getAddress(); const tokenBAddress: string = await tokenB.getAddress(); const factoryPool = await factory.getPool(tokenAAddress, tokenBAddress, FEE); return Pool__factory.connect(factoryPool, addr); - } + }; const getPoolFromPoolContract = async (poolAddress: string, addr: HardhatEthersSigner): Promise => { return Pool__factory.connect(poolAddress, addr); - } - - const fillUpLiquidity = async (pool: Pool, amount: bigint, tokenA: CustomERC20Token, tokenB: CustomERC20Token, addr: HardhatEthersSigner) => { + }; + + const fillUpLiquidity = async ( + pool: Pool, + amount: bigint, + tokenA: CustomERC20Token, + tokenB: CustomERC20Token, + addr: HardhatEthersSigner + ) => { const poolAddress = await pool.getAddress(); // Transfer tokens to addr and set approval @@ -42,7 +52,6 @@ describe('ChainbrarySwapRouter', function () { await tokenA.connect(addr).approve(poolAddress, amount); await tokenB.connect(addr).approve(poolAddress, amount); - await tokenA.transfer(addr.address, amount); await tokenB.transfer(addr.address, amount); @@ -50,7 +59,7 @@ describe('ChainbrarySwapRouter', function () { await tokenB.connect(addr).approve(poolAddress, amount); await pool.connect(addr).addLiquidity(amount, amount); - } + }; async function deployRouterFixture() { const CustomERC20Token: CustomERC20Token__factory = await ethers.getContractFactory('CustomERC20Token'); @@ -98,7 +107,7 @@ describe('ChainbrarySwapRouter', function () { const tx: ContractTransactionResponse = await factory.createPool(tokenAAddress, tokenBAddress, FEE); const receipt: ContractTransactionReceipt | null = await tx.wait(); - if(!receipt) { + if (!receipt) { throw new Error('Transaction receipt not found'); } @@ -117,11 +126,12 @@ describe('ChainbrarySwapRouter', function () { async function deployRouterWithNativeTokenFixture() { const CustomERC20Token: CustomERC20Token__factory = await ethers.getContractFactory('CustomERC20Token'); - const ChainbrarySwapFactory: ChainbrarySwapFactory__factory = await ethers.getContractFactory('ChainbrarySwapFactory'); + const ChainbrarySwapFactory: ChainbrarySwapFactory__factory = + await ethers.getContractFactory('ChainbrarySwapFactory'); const ChainbrarySwapRouter: ChainbrarySwapRouter__factory = await ethers.getContractFactory('ChainbrarySwapRouter'); - + const [owner, addr1, addr2] = await ethers.getSigners(); - + // Deploy token const tokenA: CustomERC20Token = await CustomERC20Token.deploy( owner.address, @@ -134,24 +144,24 @@ describe('ChainbrarySwapRouter', function () { [], [] ); - + // Deploy factory and router const factory: ChainbrarySwapFactory = await ChainbrarySwapFactory.deploy(); const factoryAddress: string = await factory.getAddress(); const router: ChainbrarySwapRouter = await ChainbrarySwapRouter.deploy(); await router.initialize(factoryAddress, addr1.address); - + // Deploy a pool from the factory with native token const tokenAAddress: string = await tokenA.getAddress(); - + // Listen for the PoolCreated event and create a pool - const tx: ContractTransactionResponse = await factory.createPool(ethers.ZeroAddress, tokenAAddress,FEE); + const tx: ContractTransactionResponse = await factory.createPool(ethers.ZeroAddress, tokenAAddress, FEE); const receipt: ContractTransactionReceipt | null = await tx.wait(); - + if (!receipt) { throw new Error('Transaction receipt not found'); } - + // Extract the pool address from the event const poolCreatedEvent: LogDescription | null | undefined = receipt.logs .map((log: EventLog | Log) => factory.interface.parseLog(log)) @@ -159,12 +169,12 @@ describe('ChainbrarySwapRouter', function () { if (!poolCreatedEvent) { throw new Error('PoolCreated event not found'); } - + const poolAddress: string = poolCreatedEvent.args?.pool; - + return { router, factory, poolAddress, tokenA, owner, addr1, addr2 }; } - + it('should find the correct pool address', async () => { const { factory, tokenA, tokenB, poolAddress, addr1 } = await loadFixture(deployRouterFixture); @@ -178,7 +188,7 @@ describe('ChainbrarySwapRouter', function () { expect(tokenAAddress).to.equal(await poolInstanceFromFactory.token0()); expect(tokenBAddress).to.equal(await poolInstanceFromFactory.token1()); - // Get pool from Pool contract + // Get pool from Pool contract const poolInstanceFromPoolContract: Pool = await getPoolFromPoolContract(poolAddress, addr1); const poolAddressFromPoolContract: string = await poolInstanceFromPoolContract.getAddress(); expect(poolAddress).to.equal(poolAddressFromPoolContract); @@ -187,11 +197,10 @@ describe('ChainbrarySwapRouter', function () { // Check if the pool addresses are the same expect(poolAddressFromFactory).to.equal(poolAddressFromPoolContract); - + // Check if the tokens are the same expect(await poolInstanceFromPoolContract.token0()).to.equal(await poolInstanceFromFactory.token0()); expect(await poolInstanceFromPoolContract.token1()).to.equal(await poolInstanceFromFactory.token1()); - }); it('should initialize the ChainbrarySwapRouter with correct parameters', async () => { @@ -211,9 +220,7 @@ describe('ChainbrarySwapRouter', function () { const fees = [FEE]; const amountIn = SWAP_AMOUNT; - await expect(router.connect(addr1).getAmountsOut(amountIn, path, fees)).to.be.revertedWith( - 'Invalid reserves' - ); + await expect(router.connect(addr1).getAmountsOut(amountIn, path, fees)).to.be.revertedWith('Invalid reserves'); }); it('should fail to get amounts out if path length is invalid', async () => { @@ -268,15 +275,15 @@ describe('ChainbrarySwapRouter', function () { // Check if pool was created from factory expect(await factory.getPool(tokenAAddress, tokenBAddress, FEE)).to.equal(poolAddress); - + // Set up addr2 with tokens await tokenA.transfer(addr2.address, amountIn); await tokenA.connect(addr2).approve(routerAddress, amountIn); - + // Calculate the amountOut manually using the same logic as the contract - const amountInWithFee: bigint = BigInt(amountIn) * BigInt(1000000 - FEE) / BigInt(1000000); + const amountInWithFee: bigint = (BigInt(amountIn) * BigInt(1000000 - FEE)) / BigInt(1000000); const expectedAmountOut: bigint = (amountInWithFee * reserve1BeforeSwap) / (reserve0BeforeSwap + amountInWithFee); - + // Get the output amounts from the router contract const routerInstance: ChainbrarySwapRouter = ChainbrarySwapRouter__factory.connect(routerAddress, addr2); const amountsOut: bigint[] = await routerInstance.getAmountsOut(amountIn, path, fees); @@ -287,92 +294,97 @@ describe('ChainbrarySwapRouter', function () { const balanceBefore: bigint = await tokenB.balanceOf(addr2.address); expect(balanceBefore).to.be.equal(0); - + // Execute the swap if the above calculations are consistent await router.connect(addr2).swapExactTokensForTokens(amountIn, amountOutMin, path, fees, addr2.address); - + // Check balances after swap const balanceAfter: bigint = await tokenB.balanceOf(addr2.address); expect(balanceAfter).to.be.equal(expectedAmountOut); }); it('should execute a token swap with a native token successfully', async () => { + const { router, factory, poolAddress, tokenA, addr1, addr2 } = await loadFixture( + deployRouterWithNativeTokenFixture + ); - const { router, factory, poolAddress, tokenA, addr1, addr2 } = await loadFixture(deployRouterWithNativeTokenFixture); + const tokenAAddress: string = await tokenA.getAddress(); + const poolForAddr1: Pool = await getPoolFromPoolContract(poolAddress, addr1); + const poolForAddr2: Pool = await getPoolFromPoolContract(poolAddress, addr2); - const tokenAAddress: string = await tokenA.getAddress(); - const poolForAddr1: Pool = await getPoolFromPoolContract(poolAddress, addr1); - const poolForAddr2: Pool = await getPoolFromPoolContract(poolAddress, addr2); + const path: string[] = [ethers.ZeroAddress, tokenAAddress]; + const fees: number[] = [FEE]; + const amountIn: bigint = SWAP_AMOUNT; // Increase the swap amount + const amountOutMin: number = 1; // Set to 1 to ensure the output is greater than zero + const liquidity: bigint[] = [INITIAL_LIQUIDITY_0, INITIAL_LIQUIDITY_1]; - const path: string[] = [ethers.ZeroAddress, tokenAAddress]; - const fees: number[] = [FEE]; - const amountIn: bigint = SWAP_AMOUNT; // Increase the swap amount - const amountOutMin: number = 1; // Set to 1 to ensure the output is greater than zero - const liquidity: bigint[] = [INITIAL_LIQUIDITY_0, INITIAL_LIQUIDITY_1]; + const routerAddress: string = await router.getAddress(); - const routerAddress: string = await router.getAddress(); + // Transfer tokens to addr1 and set approval and liquidity + await tokenA.transfer(addr1.address, liquidity[0]); + await tokenA.connect(addr1).approve(poolAddress, liquidity[0]); - // Transfer tokens to addr1 and set approval and liquidity - await tokenA.transfer(addr1.address, liquidity[0]); - await tokenA.connect(addr1).approve(poolAddress, liquidity[0]); + // Add liquidity to the pool + const addLiquidityTx = await poolForAddr1 + .connect(addr1) + .addLiquidity(liquidity[0], liquidity[1], { value: liquidity[0] }); + await addLiquidityTx.wait(); - - // Add liquidity to the pool - const addLiquidityTx = await poolForAddr1.connect(addr1).addLiquidity(liquidity[0], liquidity[1], { value: liquidity[0] }); - await addLiquidityTx.wait(); + // Get reserves + const reserve0BeforeSwap: bigint = await poolForAddr1.reserve0(); + const reserve1BeforeSwap: bigint = await poolForAddr1.reserve1(); + const fee: bigint = await poolForAddr1.fee(); + expect(reserve0BeforeSwap).to.be.equal(BigInt(liquidity[0])); + expect(reserve1BeforeSwap).to.be.equal(BigInt(liquidity[1])); + expect(fee).to.be.equal(BigInt(FEE)); - // Get reserves - const reserve0BeforeSwap: bigint = await poolForAddr1.reserve0(); - const reserve1BeforeSwap: bigint = await poolForAddr1.reserve1(); - const fee: bigint = await poolForAddr1.fee(); - expect(reserve0BeforeSwap).to.be.equal(BigInt(liquidity[0])); - expect(reserve1BeforeSwap).to.be.equal(BigInt(liquidity[1])); - expect(fee).to.be.equal(BigInt(FEE)); + // Check if pool was created from factory + expect(await factory.getPool(tokenAAddress, ethers.ZeroAddress, FEE)).to.equal(poolAddress); - // Check if pool was created from factory - expect(await factory.getPool(tokenAAddress, ethers.ZeroAddress, FEE)).to.equal(poolAddress); + // Swap from native token to ERC20 token + const balanceBefore: bigint = await tokenA.balanceOf(addr2.address); + expect(balanceBefore).to.be.equal(0); - // Swap from native token to ERC20 token - const balanceBefore: bigint = await tokenA.balanceOf(addr2.address); - expect(balanceBefore).to.be.equal(0); + // Check if the pool has the correct token addresses + const token0 = await poolForAddr2.token0(); + const token1 = await poolForAddr2.token1(); + expect(token0).to.be.equal(ethers.ZeroAddress); + expect(token1).to.be.equal(tokenAAddress); - // Check if the pool has the correct token addresses - const token0 = await poolForAddr2.token0(); - const token1 = await poolForAddr2.token1(); - expect(token0).to.be.equal(ethers.ZeroAddress); - expect(token1).to.be.equal(tokenAAddress); + // check balance of the pool + const poolBalance = await ethers.provider.getBalance(poolAddress); + expect(poolBalance).to.be.equal(liquidity[0]); - // check balance of the pool - const poolBalance = await ethers.provider.getBalance(poolAddress); - expect(poolBalance).to.be.equal(liquidity[0]); + // Execute the swap from pool + const halfAmountIn: bigint = amountIn / BigInt(2); + expect(halfAmountIn).to.be.gt(0); - // Execute the swap from pool - const halfAmountIn: bigint = amountIn / BigInt(2); - expect(halfAmountIn).to.be.gt(0); + // Calculate the amountOut manually using the same logic as the contract + const amountInWithFee: bigint = (BigInt(halfAmountIn) * BigInt(1000000 - FEE)) / BigInt(1000000); + const expectedAmountOut: bigint = (amountInWithFee * reserve1BeforeSwap) / (reserve0BeforeSwap + amountInWithFee); - // Calculate the amountOut manually using the same logic as the contract - const amountInWithFee: bigint = BigInt(halfAmountIn) * BigInt(1000000 - FEE) / BigInt(1000000); - const expectedAmountOut: bigint = (amountInWithFee * reserve1BeforeSwap) / (reserve0BeforeSwap + amountInWithFee); - - // Get the output amounts from the router contract - const routerInstance: ChainbrarySwapRouter = ChainbrarySwapRouter__factory.connect(routerAddress, addr2); - const amountsOut: bigint[] = await routerInstance.getAmountsOut(halfAmountIn, path, fees); - const amountOut: bigint = amountsOut[1]; + // Get the output amounts from the router contract + const routerInstance: ChainbrarySwapRouter = ChainbrarySwapRouter__factory.connect(routerAddress, addr2); + const amountsOut: bigint[] = await routerInstance.getAmountsOut(halfAmountIn, path, fees); + const amountOut: bigint = amountsOut[1]; - // Compare manual calculation with the contract result - expect(expectedAmountOut).to.be.equal(amountOut); + // Compare manual calculation with the contract result + expect(expectedAmountOut).to.be.equal(amountOut); - const balanceOfERC20TokenBefore: bigint = await tokenA.balanceOf(addr2.address); + const balanceOfERC20TokenBefore: bigint = await tokenA.balanceOf(addr2.address); - // Execute the swap if the above calculations are consistent - await router.connect(addr2).swapExactTokensForTokens(halfAmountIn, amountOutMin, path, fees, addr2.address, { value: halfAmountIn }); + // Execute the swap if the above calculations are consistent + await router + .connect(addr2) + .swapExactTokensForTokens(halfAmountIn, amountOutMin, path, fees, addr2.address, { value: halfAmountIn }); - // Check balances after swap - const balanceOfERC20TokenAfter: bigint = await tokenA.balanceOf(addr2.address); + // Check balances after swap + const balanceOfERC20TokenAfter: bigint = await tokenA.balanceOf(addr2.address); - expect(balanceOfERC20TokenAfter).to.be.equal(expectedAmountOut + balanceOfERC20TokenBefore); + expect(balanceOfERC20TokenAfter).to.be.equal(expectedAmountOut + balanceOfERC20TokenBefore); }); + it('should fail to execute a swap if output is less than minimum specified', async () => { const { factory, router, tokenA, tokenB, addr1 } = await loadFixture(deployRouterFixture); @@ -396,5 +408,4 @@ describe('ChainbrarySwapRouter', function () { router.connect(addr1).swapExactTokensForTokens(amountIn, amountOutMin, path, fees, addr1.address) ).to.be.revertedWith('Insufficient output amount'); }); - }); From bc4e17de1291e49f02d194c45ac4481ebc9dd4e7 Mon Sep 17 00:00:00 2001 From: etsraphael Date: Mon, 23 Dec 2024 16:59:07 +1100 Subject: [PATCH 09/12] Feature: Add receive function to ChainbrarySwapRouter and improve token transfer handling in Pool contract --- .../contracts/dex/ChainbrarySwapRouter.sol | 5 + SmartContracts/contracts/dex/Pool.sol | 3 +- SmartContracts/test/ChainbrarySwapRouter.ts | 94 ++++++++++++++++++- 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/SmartContracts/contracts/dex/ChainbrarySwapRouter.sol b/SmartContracts/contracts/dex/ChainbrarySwapRouter.sol index 6b0d0999..f4a0b817 100644 --- a/SmartContracts/contracts/dex/ChainbrarySwapRouter.sol +++ b/SmartContracts/contracts/dex/ChainbrarySwapRouter.sol @@ -18,6 +18,7 @@ contract ChainbrarySwapRouter is Ownable, ReentrancyGuard, Initializable { event CrossChainSwapInitiated(address indexed sender, address indexed receiver, bytes32 messageId); event CrossChainSwapReceived(address indexed receiver, bytes32 messageId); + event Received(address sender, uint amount); constructor() Ownable(_msgSender()) {} @@ -193,4 +194,8 @@ contract ChainbrarySwapRouter is Ownable, ReentrancyGuard, Initializable { function updateCCIPRouter(address _ccipRouter) external onlyOwner { ccipRouter = _ccipRouter; } + + receive() external payable { + emit Received(_msgSender(), msg.value); + } } diff --git a/SmartContracts/contracts/dex/Pool.sol b/SmartContracts/contracts/dex/Pool.sol index 9b9b43aa..22d09286 100644 --- a/SmartContracts/contracts/dex/Pool.sol +++ b/SmartContracts/contracts/dex/Pool.sol @@ -153,7 +153,8 @@ contract Pool is Ownable, ReentrancyGuard, Initializable { // Transfer the output tokens to the recipient if (tokenOut == address(0)) { - payable(to).transfer(amountOut); + bool success = payable(to).send(amountOut); + require(success, "Transfer failed"); } else { IERC20(tokenOut).safeTransfer(to, amountOut); } diff --git a/SmartContracts/test/ChainbrarySwapRouter.ts b/SmartContracts/test/ChainbrarySwapRouter.ts index 57060162..becf95e5 100644 --- a/SmartContracts/test/ChainbrarySwapRouter.ts +++ b/SmartContracts/test/ChainbrarySwapRouter.ts @@ -384,7 +384,6 @@ describe('ChainbrarySwapRouter', function () { expect(balanceOfERC20TokenAfter).to.be.equal(expectedAmountOut + balanceOfERC20TokenBefore); }); - it('should fail to execute a swap if output is less than minimum specified', async () => { const { factory, router, tokenA, tokenB, addr1 } = await loadFixture(deployRouterFixture); @@ -408,4 +407,97 @@ describe('ChainbrarySwapRouter', function () { router.connect(addr1).swapExactTokensForTokens(amountIn, amountOutMin, path, fees, addr1.address) ).to.be.revertedWith('Insufficient output amount'); }); + + it('should execute a token swap from ERC20 to native token successfully', async () => { + const { router, factory, poolAddress, tokenA, addr1, addr2 } = await loadFixture( + deployRouterWithNativeTokenFixture + ); + + const tokenAAddress: string = await tokenA.getAddress(); + const poolForAddr1: Pool = await getPoolFromPoolContract(poolAddress, addr1); + const poolForAddr2: Pool = await getPoolFromPoolContract(poolAddress, addr2); + + const path: string[] = [tokenAAddress, ethers.ZeroAddress]; + const fees: number[] = [FEE]; + const amountIn: bigint = SWAP_AMOUNT; + const amountOutMin: number = 1; + const liquidity: bigint[] = [INITIAL_LIQUIDITY_0, INITIAL_LIQUIDITY_1]; + + const routerAddress: string = await router.getAddress(); + + // Transfer tokens to addr1 and addr2 for liquidity and swap + await tokenA.transfer(addr1.address, liquidity[0]); + await tokenA.transfer(addr2.address, amountIn); + await tokenA.connect(addr1).approve(poolAddress, liquidity[0]); + await tokenA.connect(addr2).approve(routerAddress, amountIn); + + // Add liquidity to the pool + const addLiquidityTx = await poolForAddr1 + .connect(addr1) + .addLiquidity(liquidity[0], liquidity[1], { value: liquidity[1] }); + await addLiquidityTx.wait(); + + // Get reserves + const reserve0BeforeSwap: bigint = await poolForAddr1.reserve0(); + const reserve1BeforeSwap: bigint = await poolForAddr1.reserve1(); + const fee: bigint = await poolForAddr1.fee(); + expect(reserve0BeforeSwap).to.be.equal(BigInt(liquidity[0])); + expect(reserve1BeforeSwap).to.be.equal(BigInt(liquidity[1])); + expect(fee).to.be.equal(BigInt(FEE)); + + // Check if pool was created from factory + expect(await factory.getPool(tokenAAddress, ethers.ZeroAddress, FEE)).to.equal(poolAddress); + + // Record initial ETH balance + const ethBalanceBefore: bigint = await ethers.provider.getBalance(addr2.address); + + // Check if the pool has the correct token addresses + const token0 = await poolForAddr2.token0(); + const token1 = await poolForAddr2.token1(); + expect(token0).to.be.equal(ethers.ZeroAddress); + expect(token1).to.be.equal(tokenAAddress); + + // Check balance of the pool + const poolBalance = await ethers.provider.getBalance(poolAddress); + expect(poolBalance).to.be.equal(liquidity[1]); + + // Execute the swap from pool + const halfAmountIn: bigint = amountIn / BigInt(2); + expect(halfAmountIn).to.be.gt(0); + + // Calculate the amountOut manually using the same logic as the contract + const amountInWithFee: bigint = (BigInt(halfAmountIn) * BigInt(1000000 - FEE)) / BigInt(1000000); + const expectedAmountOut: bigint = (amountInWithFee * reserve1BeforeSwap) / (reserve0BeforeSwap + amountInWithFee); + + // Get the output amounts from the router contract + const routerInstance: ChainbrarySwapRouter = ChainbrarySwapRouter__factory.connect(routerAddress, addr2); + const amountsOut: bigint[] = await routerInstance.getAmountsOut(halfAmountIn, path, fees); + const amountOut: bigint = amountsOut[1]; + + // Compare manual calculation with the contract result + expect(expectedAmountOut).to.be.equal(amountOut); + + // Get erc20 token balance before swap + const balanceOfERC20TokenBefore: bigint = await tokenA.balanceOf(addr2.address); + + // Approve the router to spend the erc20 token + await tokenA.connect(addr2).approve(routerAddress, halfAmountIn); + + // Execute the swap + const swapTx = await router + .connect(addr2) + .swapExactTokensForTokens(halfAmountIn, amountOutMin, path, fees, addr2.address); + await swapTx.wait(); + + // Check balances after swap + const ethBalanceAfter: bigint = await ethers.provider.getBalance(addr2.address); + const ethDifference = ethBalanceAfter - ethBalanceBefore; + + // Account for gas costs in the comparison + expect(ethDifference).to.be.closeTo(expectedAmountOut, BigInt(1e16)); // Allow for gas costs + + // Check balance of the ERC20 token after swap + const balanceOfERC20TokenAfter: bigint = await tokenA.balanceOf(addr2.address); + expect(balanceOfERC20TokenAfter).to.be.equal(balanceOfERC20TokenBefore - halfAmountIn); + }); }); From 9d110d6d23847ab8380d031a585671a2c963df8b Mon Sep 17 00:00:00 2001 From: etsraphael Date: Mon, 23 Dec 2024 17:01:51 +1100 Subject: [PATCH 10/12] Feature: Update ChainbrarySwapRouter test to accurately calculate transaction costs and ensure correct ETH balance after swaps --- SmartContracts/test/ChainbrarySwapRouter.ts | 22 +++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/SmartContracts/test/ChainbrarySwapRouter.ts b/SmartContracts/test/ChainbrarySwapRouter.ts index becf95e5..3dd49bc0 100644 --- a/SmartContracts/test/ChainbrarySwapRouter.ts +++ b/SmartContracts/test/ChainbrarySwapRouter.ts @@ -408,7 +408,7 @@ describe('ChainbrarySwapRouter', function () { ).to.be.revertedWith('Insufficient output amount'); }); - it('should execute a token swap from ERC20 to native token successfully', async () => { + it.only('should execute a token swap from ERC20 to native token successfully', async () => { const { router, factory, poolAddress, tokenA, addr1, addr2 } = await loadFixture( deployRouterWithNativeTokenFixture ); @@ -448,9 +448,6 @@ describe('ChainbrarySwapRouter', function () { // Check if pool was created from factory expect(await factory.getPool(tokenAAddress, ethers.ZeroAddress, FEE)).to.equal(poolAddress); - // Record initial ETH balance - const ethBalanceBefore: bigint = await ethers.provider.getBalance(addr2.address); - // Check if the pool has the correct token addresses const token0 = await poolForAddr2.token0(); const token1 = await poolForAddr2.token1(); @@ -483,18 +480,27 @@ describe('ChainbrarySwapRouter', function () { // Approve the router to spend the erc20 token await tokenA.connect(addr2).approve(routerAddress, halfAmountIn); + // Record initial ETH balance + const ethBalanceBefore: bigint = await ethers.provider.getBalance(addr2.address); + // Execute the swap - const swapTx = await router + const tx1 = await router .connect(addr2) .swapExactTokensForTokens(halfAmountIn, amountOutMin, path, fees, addr2.address); - await swapTx.wait(); + const receipt = await tx1.wait(); + if (!receipt) return; + + // Calculate the cost of the transaction + const gasUsed: BigNumber = new BigNumber(receipt.gasUsed.toString()); + const gasPrice: BigNumber = new BigNumber(tx1.gasPrice.toString()); + const tx1Cost: BigNumber = gasUsed.times(gasPrice); + const tx1CostBigInt: bigint = BigInt(tx1Cost.toString()); // Check balances after swap const ethBalanceAfter: bigint = await ethers.provider.getBalance(addr2.address); - const ethDifference = ethBalanceAfter - ethBalanceBefore; // Account for gas costs in the comparison - expect(ethDifference).to.be.closeTo(expectedAmountOut, BigInt(1e16)); // Allow for gas costs + expect(ethBalanceAfter).to.be.equal(ethBalanceBefore + expectedAmountOut - tx1CostBigInt); // Check balance of the ERC20 token after swap const balanceOfERC20TokenAfter: bigint = await tokenA.balanceOf(addr2.address); From e2be9473b4b96c6673d090151cdab618aa62ebef Mon Sep 17 00:00:00 2001 From: etsraphael Date: Tue, 24 Dec 2024 12:44:44 +1100 Subject: [PATCH 11/12] Feature: Refactor liquidity withdrawal calculations in Pool contract for accuracy and improve token swap test in ChainbrarySwapRouter --- SmartContracts/contracts/dex/Pool.sol | 13 +- SmartContracts/test/ChainbrarySwapRouter.ts | 2 +- SmartContracts/test/Pool.ts | 187 +++++++++++++------- 3 files changed, 133 insertions(+), 69 deletions(-) diff --git a/SmartContracts/contracts/dex/Pool.sol b/SmartContracts/contracts/dex/Pool.sol index 22d09286..4ffc7cdc 100644 --- a/SmartContracts/contracts/dex/Pool.sol +++ b/SmartContracts/contracts/dex/Pool.sol @@ -81,16 +81,17 @@ contract Pool is Ownable, ReentrancyGuard, Initializable { require(userLiquidity0 > 0 && userLiquidity1 > 0, "No liquidity provided by user"); - uint256 amount0 = Math.min(userLiquidity0, (liquidity * reserve0) / (reserve0 + reserve1)); - uint256 amount1 = Math.min(userLiquidity1, (liquidity * reserve1) / (reserve0 + reserve1)); + uint256 totalLiquidity = userLiquidity0 + userLiquidity1; + uint256 amount0 = (liquidity * reserve0) / totalLiquidity; + uint256 amount1 = (liquidity * reserve1) / totalLiquidity; require(amount0 > 0 && amount1 > 0, "Insufficient liquidity to withdraw"); reserve0 -= amount0; reserve1 -= amount1; - liquidityProvided0[_msgSender()] -= amount0; - liquidityProvided1[_msgSender()] -= amount1; + liquidityProvided0[_msgSender()] -= (liquidity * userLiquidity0) / totalLiquidity; + liquidityProvided1[_msgSender()] -= (liquidity * userLiquidity1) / totalLiquidity; if (token0 == address(0)) { payable(_msgSender()).transfer(amount0); @@ -144,10 +145,10 @@ contract Pool is Ownable, ReentrancyGuard, Initializable { // Update the reserves based on the input and output amounts if (tokenIn == token0) { - reserve0 += amountIn; + reserve0 += amountInWithFee; reserve1 -= amountOut; } else { - reserve1 += amountIn; + reserve1 += amountInWithFee; reserve0 -= amountOut; } diff --git a/SmartContracts/test/ChainbrarySwapRouter.ts b/SmartContracts/test/ChainbrarySwapRouter.ts index 3dd49bc0..beb51d59 100644 --- a/SmartContracts/test/ChainbrarySwapRouter.ts +++ b/SmartContracts/test/ChainbrarySwapRouter.ts @@ -408,7 +408,7 @@ describe('ChainbrarySwapRouter', function () { ).to.be.revertedWith('Insufficient output amount'); }); - it.only('should execute a token swap from ERC20 to native token successfully', async () => { + it('should execute a token swap from ERC20 to native token successfully', async () => { const { router, factory, poolAddress, tokenA, addr1, addr2 } = await loadFixture( deployRouterWithNativeTokenFixture ); diff --git a/SmartContracts/test/Pool.ts b/SmartContracts/test/Pool.ts index 42f93d60..d542c8a6 100644 --- a/SmartContracts/test/Pool.ts +++ b/SmartContracts/test/Pool.ts @@ -73,7 +73,7 @@ describe('Pool', function () { const { poolInstance, owner, addr1, addr2 } = await deployPoolFixture(ethers.ZeroAddress, tokenAAddress); return { poolInstance, tokenA, owner, addr1, addr2 }; - } + }; it('should create a pool with the native token', async () => { const { token: tokenA } = await loadFixture(deployTokenAFixture); @@ -88,107 +88,109 @@ describe('Pool', function () { it('should execute a swap successfully with ERC20 tokens', async () => { const { poolInstance, tokenA, tokenB, addr1, addr2, owner } = await loadFixture(deployPoolWithTokensFixture); - + const poolAddress: string = await poolInstance.getAddress(); const tokenAAddress: string = await tokenA.getAddress(); - + // Transfer tokens to user 1 await tokenA.transfer(addr1.address, INITIAL_LIQUIDITY_0); await tokenB.transfer(addr1.address, INITIAL_LIQUIDITY_1); - + // Transfer tokens to user 2 await tokenA.transfer(addr2.address, SWAP_AMOUNT); - + // Approve tokens for transfer await tokenA.connect(addr1).approve(poolAddress, INITIAL_LIQUIDITY_0); await tokenB.connect(addr1).approve(poolAddress, INITIAL_LIQUIDITY_1); - + // Add liquidity to the pool await poolInstance.connect(addr1).addLiquidity(INITIAL_LIQUIDITY_0, INITIAL_LIQUIDITY_1); - + // Approve tokens for swap await tokenA.connect(addr2).approve(poolAddress, SWAP_AMOUNT); - + // Calculate expected output based on current reserves and swap amount const reserve0BeforeSwap: bigint = await poolInstance.reserve0(); const reserve1BeforeSwap: bigint = await poolInstance.reserve1(); - + // Calculate the amountOut manually using the same logic as the contract - const amountInWithFee: bigint = BigInt(SWAP_AMOUNT) * BigInt(1000000 - FEE) / BigInt(1000000); + const amountInWithFee: bigint = (BigInt(SWAP_AMOUNT) * BigInt(1000000 - FEE)) / BigInt(1000000); const expectedAmountOut: bigint = (amountInWithFee * reserve1BeforeSwap) / (reserve0BeforeSwap + amountInWithFee); - + // Get addr2's balance before swap const addr2BalanceBeforeSwap: bigint = await tokenB.balanceOf(addr2.address); - + // Swap token0 for token1 await poolInstance.connect(addr2).swap(SWAP_AMOUNT, tokenAAddress, addr2.address); - + const reserve0AfterSwap: bigint = await poolInstance.reserve0(); const reserve1AfterSwap: bigint = await poolInstance.reserve1(); - + // Verify reserves after swap - expect(reserve0AfterSwap).to.equal(reserve0BeforeSwap + BigInt(SWAP_AMOUNT)); + expect(reserve0AfterSwap).to.equal(reserve0BeforeSwap + amountInWithFee); expect(reserve1AfterSwap).to.equal(reserve1BeforeSwap - expectedAmountOut); - + // Get addr2's balance after swap const addr2BalanceAfterSwap: bigint = await tokenB.balanceOf(addr2.address); - + // Verify addr2's balance increased by the expected amount out expect(addr2BalanceAfterSwap).to.equal(addr2BalanceBeforeSwap + expectedAmountOut); }); it('should execute a swap successfully with a native token', async () => { const { poolInstance, tokenA, addr1, addr2, owner } = await loadFixture(deployPoolWithNativeTokenFixture); - + const poolAddress: string = await poolInstance.getAddress(); const tokenAAddress: string = await tokenA.getAddress(); - + // Transfer tokens to user 1 await tokenA.transfer(addr1.address, INITIAL_LIQUIDITY_1); - + // Transfer tokens to user 2 await tokenA.transfer(addr2.address, SWAP_AMOUNT); - + // Approve tokens for transfer await tokenA.connect(addr1).approve(poolAddress, INITIAL_LIQUIDITY_1); - + // Add liquidity to the pool (Native token and TokenA) - await poolInstance.connect(addr1).addLiquidity(INITIAL_NATIVE_LIQUIDITY, INITIAL_LIQUIDITY_1, { value: INITIAL_NATIVE_LIQUIDITY }); - + await poolInstance + .connect(addr1) + .addLiquidity(INITIAL_NATIVE_LIQUIDITY, INITIAL_LIQUIDITY_1, { value: INITIAL_NATIVE_LIQUIDITY }); + // Approve tokens for swap await tokenA.connect(addr2).approve(poolAddress, SWAP_AMOUNT); - + // Calculate expected output based on current reserves and swap amount const reserve0BeforeSwap: bigint = await poolInstance.reserve0(); // Native token reserve const reserve1BeforeSwap: bigint = await poolInstance.reserve1(); // TokenA reserve - + // Calculate the amountOut manually using the same logic as the contract - const amountInWithFee: bigint = BigInt(SWAP_AMOUNT) * BigInt(1000000 - FEE) / BigInt(1000000); + const amountInWithFee: bigint = (BigInt(SWAP_AMOUNT) * BigInt(1000000 - FEE)) / BigInt(1000000); const expectedAmountOut: bigint = (amountInWithFee * reserve0BeforeSwap) / (reserve1BeforeSwap + amountInWithFee); - + // Get addr2's balance of native token before swap const addr2BalanceBeforeSwap: bigint = await ethers.provider.getBalance(addr2.address); - + // Swap TokenA for native token const tx = await poolInstance.connect(addr2).swap(SWAP_AMOUNT, tokenAAddress, addr2.address); const receipt = await tx.wait(); - if(!receipt) return; - + if (!receipt) return; + // Calculate gas used and subtract from addr2's native token balance const gasUsed: bigint = BigInt(receipt.gasUsed) * BigInt(tx.gasPrice || 0); const addr2BalanceAfterGas: bigint = addr2BalanceBeforeSwap - gasUsed; - + const reserve0AfterSwap: bigint = await poolInstance.reserve0(); const reserve1AfterSwap: bigint = await poolInstance.reserve1(); - + // Verify reserves after swap expect(reserve0AfterSwap).to.equal(reserve0BeforeSwap - expectedAmountOut); - expect(reserve1AfterSwap).to.equal(reserve1BeforeSwap + BigInt(SWAP_AMOUNT)); - + expect(reserve1AfterSwap).to.equal(reserve1BeforeSwap + amountInWithFee); + // Get addr2's balance of native token after swap const addr2BalanceAfterSwap: bigint = await ethers.provider.getBalance(addr2.address); - + // Verify addr2's balance increased by the expected amount out (minus gas used) expect(addr2BalanceAfterSwap).to.equal(addr2BalanceAfterGas + expectedAmountOut); }); @@ -238,7 +240,7 @@ describe('Pool', function () { it('should add liquidity successfully with a native token', async () => { const { poolInstance, tokenA, addr1 } = await loadFixture(deployPoolWithNativeTokenFixture); - + const poolAddress: string = await poolInstance.getAddress(); // Save initial native balance @@ -248,7 +250,10 @@ describe('Pool', function () { const initialTokenBalance: bigint = await tokenA.balanceOf(addr1.address); // Utility function to calculate gas fees - async function calculateGasUsedForTransaction(tx: ContractTransactionResponse, receipt: ContractTransactionReceipt): Promise { + async function calculateGasUsedForTransaction( + tx: ContractTransactionResponse, + receipt: ContractTransactionReceipt + ): Promise { const gasUsed = BigInt(receipt.gasUsed); const gasPrice = BigInt(tx.gasPrice); return gasUsed * gasPrice; @@ -260,47 +265,51 @@ describe('Pool', function () { // Transfer tokens to user const transferTx = await tokenA.transfer(addr1.address, INITIAL_LIQUIDITY_1); const transferReceipt = await transferTx.wait(); - if(!transferReceipt) return; + if (!transferReceipt) return; // Check token balance after transfer const tokenBalanceAfterTransfer: bigint = await tokenA.balanceOf(addr1.address); expect(tokenBalanceAfterTransfer).to.equal(initialTokenBalance + INITIAL_LIQUIDITY_1); - + // Approve TokenA for transfer const approveTx = await tokenA.connect(addr1).approve(poolAddress, INITIAL_LIQUIDITY_1); const approveReceipt = await approveTx.wait(); - if(!approveReceipt) return; + if (!approveReceipt) return; totalGasUsed += await calculateGasUsedForTransaction(approveTx, approveReceipt); expect(await ethers.provider.getBalance(addr1.address)).to.equal(nativeBalanceBefore - totalGasUsed); // Check allowance after approve const allowanceAfterApprove: bigint = await tokenA.allowance(addr1.address, poolAddress); expect(allowanceAfterApprove).to.equal(INITIAL_LIQUIDITY_1); - + // Add liquidity to the pool (Native token and TokenA) - const addLiquidityTx = await poolInstance.connect(addr1).addLiquidity(INITIAL_NATIVE_LIQUIDITY, INITIAL_LIQUIDITY_1, { value: INITIAL_NATIVE_LIQUIDITY }); + const addLiquidityTx = await poolInstance + .connect(addr1) + .addLiquidity(INITIAL_NATIVE_LIQUIDITY, INITIAL_LIQUIDITY_1, { value: INITIAL_NATIVE_LIQUIDITY }); const addLiquidityReceipt = await addLiquidityTx.wait(); - if(!addLiquidityReceipt) return + if (!addLiquidityReceipt) return; totalGasUsed += await calculateGasUsedForTransaction(addLiquidityTx, addLiquidityReceipt); // Check native balance after adding liquidity - expect(await ethers.provider.getBalance(addr1.address)).to.equal(nativeBalanceBefore - totalGasUsed - INITIAL_NATIVE_LIQUIDITY); - + expect(await ethers.provider.getBalance(addr1.address)).to.equal( + nativeBalanceBefore - totalGasUsed - INITIAL_NATIVE_LIQUIDITY + ); + // Check pool reserves after adding liquidity const reserve0: bigint = await poolInstance.reserve0(); // Native token reserve const reserve1: bigint = await poolInstance.reserve1(); // TokenA reserve - + expect(reserve0).to.equal(INITIAL_NATIVE_LIQUIDITY); expect(reserve1).to.equal(INITIAL_LIQUIDITY_1); - + // Verify addr1's final balances const addr1NativeBalance: bigint = await ethers.provider.getBalance(addr1.address); const addr1TokenABalance: bigint = await tokenA.balanceOf(addr1.address); - + // Check that the remaining native token balance is correct after liquidity addition const expectedNativeBalance: bigint = BigInt(nativeBalanceBefore) - INITIAL_NATIVE_LIQUIDITY - totalGasUsed; expect(addr1NativeBalance).to.equal(expectedNativeBalance); - + // Check that the remaining TokenA balance is correct const expectedTokenABalance: bigint = tokenBalanceAfterTransfer - INITIAL_LIQUIDITY_1; expect(addr1TokenABalance).to.equal(expectedTokenABalance); @@ -360,7 +369,10 @@ describe('Pool', function () { const initialTokenBalance: bigint = await tokenA.balanceOf(addr1.address); // Utility function to calculate gas fees - async function calculateGasUsedForTransaction(tx: ContractTransactionResponse, receipt: ContractTransactionReceipt): Promise { + async function calculateGasUsedForTransaction( + tx: ContractTransactionResponse, + receipt: ContractTransactionReceipt + ): Promise { const gasUsed = BigInt(receipt.gasUsed); const gasPrice = BigInt(tx.gasPrice); return gasUsed * gasPrice; @@ -372,31 +384,37 @@ describe('Pool', function () { // Transfer tokens to user const transferTx = await tokenA.transfer(addr1.address, INITIAL_LIQUIDITY_1); const transferReceipt = await transferTx.wait(); - if(!transferReceipt) return; + if (!transferReceipt) return; // Check token balance after transfer const tokenBalanceAfterTransfer: bigint = await tokenA.balanceOf(addr1.address); expect(tokenBalanceAfterTransfer).to.equal(initialTokenBalance + INITIAL_LIQUIDITY_1); - + // Approve TokenA for transfer - const approveTx: ContractTransactionResponse = await tokenA.connect(addr1).approve(poolAddress, INITIAL_LIQUIDITY_1); + const approveTx: ContractTransactionResponse = await tokenA + .connect(addr1) + .approve(poolAddress, INITIAL_LIQUIDITY_1); const approveReceipt: ContractTransactionReceipt | null = await approveTx.wait(); - if(!approveReceipt) return; + if (!approveReceipt) return; totalGasUsed += await calculateGasUsedForTransaction(approveTx, approveReceipt); expect(await ethers.provider.getBalance(addr1.address)).to.equal(nativeBalanceBefore - totalGasUsed); // Check allowance after approve const allowanceAfterApprove: bigint = await tokenA.allowance(addr1.address, poolAddress); expect(allowanceAfterApprove).to.equal(INITIAL_LIQUIDITY_1); - + // Add liquidity to the pool (Native token and TokenA) - const addLiquidityTx: ContractTransactionResponse = await poolInstance.connect(addr1).addLiquidity(INITIAL_NATIVE_LIQUIDITY, INITIAL_LIQUIDITY_1, { value: INITIAL_NATIVE_LIQUIDITY }); + const addLiquidityTx: ContractTransactionResponse = await poolInstance + .connect(addr1) + .addLiquidity(INITIAL_NATIVE_LIQUIDITY, INITIAL_LIQUIDITY_1, { value: INITIAL_NATIVE_LIQUIDITY }); const addLiquidityReceipt: ContractTransactionReceipt | null = await addLiquidityTx.wait(); - if(!addLiquidityReceipt) return + if (!addLiquidityReceipt) return; totalGasUsed += await calculateGasUsedForTransaction(addLiquidityTx, addLiquidityReceipt); // Check native balance after adding liquidity - expect(await ethers.provider.getBalance(addr1.address)).to.equal(nativeBalanceBefore - totalGasUsed - INITIAL_NATIVE_LIQUIDITY); + expect(await ethers.provider.getBalance(addr1.address)).to.equal( + nativeBalanceBefore - totalGasUsed - INITIAL_NATIVE_LIQUIDITY + ); // Get reserves after liquidity removal const reserve0After: bigint = await poolInstance.reserve0(); @@ -405,12 +423,16 @@ describe('Pool', function () { expect(reserve1After).to.equal(INITIAL_LIQUIDITY_1); // Calculate liquidity to remove based on the proportionality of reserves - const totalLiquidity: BigNumber = new BigNumber(reserve0After.toString()).plus(new BigNumber(reserve1After.toString())); + const totalLiquidity: BigNumber = new BigNumber(reserve0After.toString()).plus( + new BigNumber(reserve1After.toString()) + ); // Remove liquidity - const removeLiquidityTx: ContractTransactionResponse = await poolInstance.connect(addr1).removeLiquidity(totalLiquidity.toFixed(0)); // Convert BigNumber to string + const removeLiquidityTx: ContractTransactionResponse = await poolInstance + .connect(addr1) + .removeLiquidity(totalLiquidity.toFixed(0)); // Convert BigNumber to string const removeLiquidityReceipt: ContractTransactionReceipt | null = await removeLiquidityTx.wait(); - if(!removeLiquidityReceipt) return + if (!removeLiquidityReceipt) return; totalGasUsed += await calculateGasUsedForTransaction(removeLiquidityTx, removeLiquidityReceipt); // // Check native balance after removing liquidity @@ -533,4 +555,45 @@ describe('Pool', function () { await poolInstance.connect(owner).swap(SWAP_AMOUNT, await tokenA.getAddress(), owner.address); }); + it('should widhdraw all liquidity successfully', async () => { + const { poolInstance, tokenA, tokenB, addr1 } = await loadFixture(deployPoolWithTokensFixture); + + const poolAddress: string = await poolInstance.getAddress(); + + // Transfer tokens to users + await tokenA.transfer(addr1.address, INITIAL_LIQUIDITY_0); + await tokenB.transfer(addr1.address, INITIAL_LIQUIDITY_1); + + // Approve tokens for transfer + await tokenA.connect(addr1).approve(poolAddress, INITIAL_LIQUIDITY_0); + await tokenB.connect(addr1).approve(poolAddress, INITIAL_LIQUIDITY_1); + + // Add liquidity to the pool + await poolInstance.connect(addr1).addLiquidity(INITIAL_LIQUIDITY_0, INITIAL_LIQUIDITY_1); + + // Make a swap before withdrawing all the liquidity + const halfAmount: bigint = BigInt(SWAP_AMOUNT) / BigInt(2); + await tokenA.transfer(addr1.address, halfAmount); + await tokenA.connect(addr1).approve(poolAddress, halfAmount); + await poolInstance.connect(addr1).swap(halfAmount, await tokenA.getAddress(), addr1.address); + + // Get reserves after adding liquidity + const liquidityProvided: [bigint, bigint] = await poolInstance.getLiquidityProvided(addr1.address); + + // Calculate liquidity to remove based on the proportionality of reserves + const totalLiquidity: BigNumber = new BigNumber(liquidityProvided[0].toString()).plus( + new BigNumber(liquidityProvided[1].toString()) + ); + + // Remove liquidity + await poolInstance.connect(addr1).removeLiquidity(totalLiquidity.toFixed(0)); + + // Get reserves after liquidity removal + const reserve0After: bigint = await poolInstance.reserve0(); + const reserve1After: bigint = await poolInstance.reserve1(); + + // Check if reserves are as expected + expect(reserve0After.toString()).to.equal('0'); + expect(reserve1After.toString()).to.equal('0'); + }); }); From a5db008abb9fd8c0650e4c063d87850a97e82b03 Mon Sep 17 00:00:00 2001 From: etsraphael Date: Fri, 27 Dec 2024 09:49:25 +1100 Subject: [PATCH 12/12] Feature: Fix gas estimation logic in DexService for addLiquidity method --- UI/src/app/shared/services/dex/dex.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/src/app/shared/services/dex/dex.service.ts b/UI/src/app/shared/services/dex/dex.service.ts index 543e9c6b..838e0c1e 100644 --- a/UI/src/app/shared/services/dex/dex.service.ts +++ b/UI/src/app/shared/services/dex/dex.service.ts @@ -138,7 +138,7 @@ export class DexService { const gasEstimate: bigint = await poolFragment.methods['addLiquidity'](amount0, amount1).estimateGas({ from, - value: address1 === ZeroAddress ? amount0 : address2 === ZeroAddress ? amount1 : '0' + value: address1 === ZeroAddress ? amount0 : address2 === ZeroAddress ? amount1 : (address1 !== ZeroAddress && address2 !== ZeroAddress ? '0' : '0') }); return poolFragment.methods['addLiquidity'](amount0, amount1)