Skip to content

Add berachain swap scaffolding #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
12 changes: 12 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/openzeppelin-foundry-upgrades"]
path = lib/openzeppelin-foundry-upgrades
url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades
[submodule "lib/solidity-stringutils"]
path = lib/solidity-stringutils
url = https://github.com/Arachnid/solidity-stringutils
[submodule "lib/openzeppelin-contracts-upgradeable"]
path = lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
26,889 changes: 313 additions & 26,576 deletions app/src/generated/graphql-env.d.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion app/src/lib/components/navigation/index.ts
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import Navigation from "./navigation.svelte"
const routes = {
transfer: { draft: false, path: "/transfer" },
// WIP:
// swap: { draft: false, path: "/swap" },
swap: { draft: false, path: "/swap" },
faucet: { draft: false, path: "/faucet" },
explorer: { draft: false, path: "/explorer" },
transfers: { draft: true, path: "/transfers" }
415 changes: 340 additions & 75 deletions app/src/lib/swap-form.svelte

Large diffs are not rendered by default.

838 changes: 838 additions & 0 deletions app/src/lib/swap/abis.ts

Large diffs are not rendered by default.

99 changes: 99 additions & 0 deletions app/src/lib/swap/action-msgs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Actions to perform on the EVM side using Abstract Action contracts
import { encodeFunctionData } from 'viem';
import { EVM_CONTRACTS, UCS01_CHANNELS } from './constants';
import { bexActionsAbi, ibcTokenActionsAbi } from './abis';
import { cosmosToEvmAddress } from '$lib/wallet/utilities/derive-address';
import { isSupportedChainId } from './helpers';
import type { EvmAddress } from '$lib/wallet/types';

type EvmMsg = {
call: {
to: string;
data: string;
allow_failure?: boolean;
value?: boolean;
}
} | {
delegate_call: {
to: string;
data: string;
allow_failure?: boolean;
value?: boolean;
}
}

export const sendFullBalanceBackMsg = ({ evmChainId, tokens, unionReceiverAddress }: { evmChainId: string, tokens: string[], unionReceiverAddress: `union${string}` }): EvmMsg => {
if (!isSupportedChainId(evmChainId)) {
throw new Error(`Unsupported chain id: ${evmChainId}`)
}

const ibcActionsAddress = EVM_CONTRACTS[evmChainId].ibc_actions
return {
delegate_call: {
to: ibcActionsAddress,
data: encodeFunctionData({
abi: ibcTokenActionsAbi,
functionName: "ibcSendPercentage",
args: [
ibcActionsAddress,
{
tokens: tokens.map((token) => ({
denom: token,
// 100%
percentage: 1e6
})),
// TODO: EVM UCS channel id
channelId: UCS01_CHANNELS[evmChainId],
// TODO: User hex address on Union
receiver: cosmosToEvmAddress(unionReceiverAddress),
// receiver: "15cbba30256b961c37b3fd7224523abdf562fd72",
extension: ""
}
]
}).slice(2),
allow_failure: true
}
}
}

type SwapActionParams = { baseAsset: EvmAddress, quoteAsset: EvmAddress, swapAmount: bigint }

export const swapActionMsg = ({ evmChainId, baseAsset, quoteAsset, swapAmount }: { evmChainId: string } & SwapActionParams): EvmMsg => {
switch (evmChainId) {
case '80084': {
return bexSwapActionMsg({ baseAsset, quoteAsset, swapAmount })
}
default:
throw new Error(`Unsupported chain id: ${evmChainId}`)
}
}

/**
* Actions to perform on bexswap specifically.
*/
export const bexSwapActionMsg = ({ baseAsset, quoteAsset, swapAmount }: SwapActionParams): EvmMsg => {

const bexSwapActionsAddress = EVM_CONTRACTS['80084'].bex_actions

return {
delegate_call: {
to: bexSwapActionsAddress,
data: encodeFunctionData({
abi: bexActionsAbi,
functionName: "swap",
args: [
bexSwapActionsAddress,
{
base: baseAsset,
quote: quoteAsset,
// amount?
quantity: swapAmount,
isBuy: true
}
]
}).slice(2),
allow_failure: true
},
}
}

55 changes: 55 additions & 0 deletions app/src/lib/swap/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
type SupportedChainId = keyof typeof EVM_CONTRACTS
export type SupportedEvmVoiceChainId = SupportedChainId

export const EVM_CONTRACTS = {
['80084']: {
evm_voice: '0xAbAbdA590fb700d753a5b2134f653269D790A245',
bex_actions: '0x522C5cA7440573Cf11F567347Bcc09Bb9292968b',
ibc_actions: '0x00d14FB2734690E18D06f62656cbAb048B694AE1',
ibc_handler: "0x851c0EB711fe5C7c8fe6dD85d9A0254C8dd11aFD",
ucs01_handler: "0x6F270608fB562133777AF0f71F6386ffc1737C30",
croc_impact: '0xCfEa3579a06e2e9a596D311486D12B3a49a919Cd',
muno: '0x08247b1C6D6AACF6C655f711661D5810380C8385'
}
} as const

export const EVM_TO_UNION_CONNECTIONS = {
['80084']: 'connection-0'
} as const

export const SWAPPABLE_ASSETS = {
['80084']: [{
display_symbol: 'HONEY',
symbol: 'HONEY',
name: 'Honey',
address: '0x0E4aaF1351de4c0264C5c7056Ef3777b41BD8e03',
unionDenom: 'factory/union1m87a5scxnnk83wfwapxlufzm58qe2v65985exff70z95a2yr86yq7hl08h/0x6263f6409ba2cd53e84f2bc19a5e718e25b5c7e499'
},
{
display_symbol: 'UNO',
symbol: 'UNO',
name: 'Uno',
address: '0x08247b1C6D6AACF6C655f711661D5810380C8385',
unionDenom: 'muno'
}]
} as const

type SwappableAssetSymbol<T extends SupportedChainId> = typeof SWAPPABLE_ASSETS[T][number]['display_symbol'];

export const PAIRS: {[K in SupportedChainId]: Array<ReadonlyArray<SwappableAssetSymbol<K>>> } = {
['80084']: [
// ['BASE', 'QUOTE']
['UNO', 'HONEY']
]
} as const

export const UCS01_CHANNELS = {
['80084']: 'channel-3'
} as const

export const BERACHAIN_CONTRACTS = EVM_CONTRACTS['80084']

export const UNION_CONTRACTS = {
evm_note: 'union19wjl2750am9nce4tc8l4dl4lnwrvvdp2h8ueaepqu52kqn9t9pnq7k3jpp',
ucs01_forwarder: 'union1m87a5scxnnk83wfwapxlufzm58qe2v65985exff70z95a2yr86yq7hl08h'
} as const
4 changes: 4 additions & 0 deletions app/src/lib/swap/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { type SupportedEvmVoiceChainId, EVM_CONTRACTS } from './constants';


export const isSupportedChainId = (chainId: string): chainId is SupportedEvmVoiceChainId => chainId in EVM_CONTRACTS;
106 changes: 106 additions & 0 deletions app/src/lib/swap/mutations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { createMutation } from "@tanstack/svelte-query";
import { UNION_CONTRACTS } from "./constants";
import { UnionClient } from "@union/client";
import { sendFullBalanceBackMsg, swapActionMsg } from "./action-msgs";
import { toast } from "svelte-sonner";
import type { Chain } from "$lib/types";
import type { EvmAddress } from "$lib/wallet/types";

interface TransferToEvmParams {
unionClient: UnionClient;
asset: {
denom: string
amount: bigint
}
channelId: `channel-${string}`
receiver: EvmAddress;
}

export const createTransferToEvmMutation = () => createMutation({
mutationFn: async ({
unionClient: cosmosClient,
asset: { denom, amount },
channelId,
receiver,
}: TransferToEvmParams) => {
await cosmosClient.transferAssets({
kind: "cosmwasm",
instructions: [
{
contractAddress: UNION_CONTRACTS.ucs01_forwarder,
msg: {
transfer: {
channel: channelId,
receiver: receiver.slice(2),
memo: ""
}
},
funds: [{ denom: denom, amount: amount.toString() }]
}
]
});
},
onError: (error) => {
toast.error(`Transfer to EVM failed: ${error.message}`);
},
onSuccess: () => {
toast.success("Transfer to EVM completed successfully");
},
});


interface EvmSwapParams {
unionClient: UnionClient
toChain: Chain
fromAssetAddress: EvmAddress;
toAssetAddress: EvmAddress;
amount: bigint;
receiverAddrUnion: `union${string}`;
}

export const createEvmSwapMutation = () => createMutation({
mutationFn: async ({
unionClient: cosmosClient,
toChain,
toAssetAddress,
amount: parsedSwapAmount,
fromAssetAddress: baseAsset,
receiverAddrUnion: unionAddr,
}: EvmSwapParams) => {
// Perform the swap
const evmNoteMsg = [
{
contractAddress: UNION_CONTRACTS.evm_note,
msg: {
execute: {
msgs: [
swapActionMsg({
evmChainId: toChain.chain_id,
baseAsset,
quoteAsset: toAssetAddress,
swapAmount: parsedSwapAmount
}),
sendFullBalanceBackMsg({
evmChainId: toChain.chain_id,
tokens: [toAssetAddress],
unionReceiverAddress: unionAddr
}),
],
callback: null,
timeout_seconds: "5000000"
}
}
}
];

return await cosmosClient.cosmwasmMessageExecuteContract(evmNoteMsg);
},
onError: (error) => {
toast.error(`Swap failed: ${error.message}`);
},
onSuccess: (cosmosTransfer) => {
console.log(`https://explorer.nodestake.org/union-testnet/tx/${cosmosTransfer.transactionHash}`);
toast.success("Swap completed successfully");
},
});

58 changes: 58 additions & 0 deletions app/src/lib/swap/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { createQuery } from '@tanstack/svelte-query';
import { readContract } from '@wagmi/core';
import { crocImpactAbi, evmVoiceAbi } from "./abis";
import { BERACHAIN_CONTRACTS, UNION_CONTRACTS } from './constants'; // Assuming you have this import
import { config } from '$lib/wallet/evm/config';
import type { EvmAddress } from '$lib/wallet/types';

export const bexSwapEstimateQuery = ({ baseAsset, quoteAsset, baseAmount }: { baseAsset: string | undefined; quoteAsset: string | undefined; baseAmount: bigint }) => {
return createQuery({
queryKey: ['bexSwapEstimate', baseAsset, quoteAsset, baseAmount],
queryFn: async () => {
if (!baseAsset || !quoteAsset || !baseAmount) {
throw new Error('Invalid input');
}
const result = await readContract(config, {
chainId: 80084,
abi: crocImpactAbi,
address: BERACHAIN_CONTRACTS.croc_impact,
functionName: 'calcImpactCall',
args: [
baseAsset,
quoteAsset,
36000n, // poolIdx
true, // isBuy
true, // inBaseQty
baseAmount, // qty
0n, // poolTip
21267430153580247136652501917186561137n // limitPrice
],
});

return result;
},
enabled: !!baseAsset && !!quoteAsset && !!baseAmount,
});
};

export const evmProxyAddressQuery = ({ cosmosAddress, connectionId, evmChainId }: { cosmosAddress: `union${string}` | null; connectionId: string, evmChainId: string }) => (createQuery({
queryKey: ['evmProxyAddress', evmChainId, cosmosAddress],
queryFn: async () => {
if (!cosmosAddress) throw new Error('Cosmos address is required')

return readContract(config, {
chainId: 80084, // TODO: Don't hardcode this
abi: evmVoiceAbi,
address: BERACHAIN_CONTRACTS.evm_voice,
functionName: 'getExpectedProxyAddress',
args: [{
// connection from evm -> union
connection: connectionId,
// port on union
port: `wasm.${UNION_CONTRACTS.evm_note}`,
sender: cosmosAddress
}]
}) as Promise<EvmAddress>
},
enabled: !!cosmosAddress
}))
1 change: 1 addition & 0 deletions lib/forge-std
Submodule forge-std added at 1ce753
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts-upgradeable
1 change: 1 addition & 0 deletions lib/openzeppelin-foundry-upgrades
1 change: 1 addition & 0 deletions lib/solidity-stringutils
Submodule solidity-stringutils added at 4b2fcc