From f48ba9d8e61fe7a81d3658048b70d223b272c577 Mon Sep 17 00:00:00 2001 From: Tyrone Johnson Date: Tue, 7 Jul 2026 19:49:39 +0300 Subject: [PATCH 1/5] add USDT0 AMM pool --- .../MarketMakingPage.constants.ts | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/app/5_pages/MarketMakingPage/MarketMakingPage.constants.ts b/apps/frontend/src/app/5_pages/MarketMakingPage/MarketMakingPage.constants.ts index ab44c2e58..d31f493ab 100644 --- a/apps/frontend/src/app/5_pages/MarketMakingPage/MarketMakingPage.constants.ts +++ b/apps/frontend/src/app/5_pages/MarketMakingPage/MarketMakingPage.constants.ts @@ -4,7 +4,30 @@ import { COMMON_SYMBOLS } from '../../../utils/asset'; import { PromotionData } from './MarketMakingPage.types'; import { AmmLiquidityPool } from './utils/AmmLiquidityPool'; +const isAddress = (value?: string) => /^0x[a-fA-F0-9]{40}$/.test(value || ''); + +// USDT0/RBTC pool activation. Leave empty until the pool is deployed. +const USDT0_BTC_AMM_CONVERTER = '0xd107e06964112d3f70cfb386565dfbda16ae71f3'; +const USDT0_BTC_AMM_POOL_TOKEN = '0x591e07d721c2e22eeb4bf33d0b3377daca886fcc'; + +const MAINNET_AMM_USDT0_WRBTC = + isAddress(USDT0_BTC_AMM_CONVERTER) && isAddress(USDT0_BTC_AMM_POOL_TOKEN) + ? [ + new AmmLiquidityPool( + COMMON_SYMBOLS.USDT0, + COMMON_SYMBOLS.BTC, + 1, + ChainIds.RSK_MAINNET, + USDT0_BTC_AMM_CONVERTER!, + USDT0_BTC_AMM_POOL_TOKEN!, + undefined, + true, + ).setSovRewards(false), + ] + : []; + export const MAINNET_AMM = [ + ...MAINNET_AMM_USDT0_WRBTC, new AmmLiquidityPool( COMMON_SYMBOLS.DLLR, COMMON_SYMBOLS.BTC, @@ -143,8 +166,6 @@ export const MAINNET_AMM = [ ChainIds.RSK_MAINNET, '0xF1DeE3175593f4e13a2b9e09a5FaafC513c9A27F', '0xfd834bbcde8c3ac4766bf5c1f5d861400103087b', - undefined, - true, ), ]; From 659dee56d21b7f0163648cad9cd00ef7f594f359 Mon Sep 17 00:00:00 2001 From: Tyrone Johnson Date: Wed, 8 Jul 2026 10:51:06 +0300 Subject: [PATCH 2/5] tech README to push deploy --- apps/frontend/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/frontend/README.md b/apps/frontend/README.md index a3358b480..2b0cc4038 100644 --- a/apps/frontend/README.md +++ b/apps/frontend/README.md @@ -15,3 +15,4 @@ yarn --cwd apps/frontend serve To use custom environment variables locally, create a file named `.env.local` and add required values (see `.env.example` for sample variables and values). For environment variables used in deployments see `netlify.toml`. + From 43a7d25ead5c304995bd8edd2dbec48bc08573bf Mon Sep 17 00:00:00 2001 From: Tyrone Johnson Date: Wed, 8 Jul 2026 12:40:38 +0300 Subject: [PATCH 3/5] fix: USDT0 decimals handling and convert page listing USDT0 is the first non-18-decimal token (6 decimals) on the RSK convert and AMM market making flows, which assumed 18 decimals throughout. - sdk: add USDT0 to the AMM smart-router token list so it shows up on the convert page, and normalize amounts between the router's 18-decimal convention and each token's native units in quote/swap/ approve (no-op for 18-decimal tokens; unknown tokens keep the legacy 18-decimal pass-through) - sdk: treat USDT0 as a stablecoin in pair filtering (like RUSDT) - frontend: add USDT0 to the convert page stablecoins category - frontend: parse AMM deposit/withdraw input with the asset's real decimals, floor max-click values to the token precision, and convert on-chain asset A balances with real decimals in useGetUserInfo, useGetPoolsBalance and useGetPoolLiquidity - frontend: fix RUSDT->USDT0 migration capacity check comparing the 18-normalized provider balance against the native 6-decimal allowance (capacity was overstated when the balance is the binding limit) - frontend: fix full-withdraw zero display in NewPoolStatistics comparing contract units against human units --- .../RusdtMigrationNotice.tsx | 7 +- .../ConvertPage/ConvertPage.constants.ts | 1 + .../AdjustAndDepositModal.tsx | 33 +++++-- .../NewPoolStatistics/NewPoolStatistics.tsx | 3 +- .../hooks/useGetPoolsBalance.ts | 8 +- .../hooks/useGetPoolLiquidity.ts | 31 ++++-- .../MarketMakingPage/hooks/useGetUserInfo.ts | 10 +- .../sdk/src/_tests/swaps/smart-router.test.ts | 4 +- packages/sdk/src/constants.ts | 1 + .../smart-router/routes/amm-swap-route.ts | 98 ++++++++++++++++--- 10 files changed, 158 insertions(+), 38 deletions(-) diff --git a/apps/frontend/src/app/2_molecules/RusdtMigrationNotice/RusdtMigrationNotice.tsx b/apps/frontend/src/app/2_molecules/RusdtMigrationNotice/RusdtMigrationNotice.tsx index dffe0b947..ebc76ae5a 100644 --- a/apps/frontend/src/app/2_molecules/RusdtMigrationNotice/RusdtMigrationNotice.tsx +++ b/apps/frontend/src/app/2_molecules/RusdtMigrationNotice/RusdtMigrationNotice.tsx @@ -112,7 +112,12 @@ export const RusdtMigrationNotice: FC = ({ }, [blockNumber]); const migrationCapacityWei = useMemo(() => { - const balanceLimit = bigNumberic(migrationUsdtBalanceWei); + // `weiBalance` from useAssetBalance is 18-decimals-normalized while the + // allowance is read from the contract in USDT0's native 6 decimals — + // bring the balance down to native units before comparing. + const balanceLimit = bigNumberic(migrationUsdtBalanceWei).div( + RUSDT_TO_USDT_DECIMALS_MULTIPLIER, + ); const allowanceLimit = bigNumberic(migrationAllowanceWei); return balanceLimit.lt(allowanceLimit) ? balanceLimit : allowanceLimit; diff --git a/apps/frontend/src/app/5_pages/ConvertPage/ConvertPage.constants.ts b/apps/frontend/src/app/5_pages/ConvertPage/ConvertPage.constants.ts index ca13b52eb..b343dc998 100644 --- a/apps/frontend/src/app/5_pages/ConvertPage/ConvertPage.constants.ts +++ b/apps/frontend/src/app/5_pages/ConvertPage/ConvertPage.constants.ts @@ -37,6 +37,7 @@ export const SMART_ROUTER_STABLECOINS = [ COMMON_SYMBOLS.DOC, 'RDOC', COMMON_SYMBOLS.RUSDT, + COMMON_SYMBOLS.USDT0, 'USDT', 'USDC', 'DAI', diff --git a/apps/frontend/src/app/5_pages/MarketMakingPage/components/AdjustAndDepositModal/AdjustAndDepositModal.tsx b/apps/frontend/src/app/5_pages/MarketMakingPage/components/AdjustAndDepositModal/AdjustAndDepositModal.tsx index c9402ab8b..be4602eb5 100644 --- a/apps/frontend/src/app/5_pages/MarketMakingPage/components/AdjustAndDepositModal/AdjustAndDepositModal.tsx +++ b/apps/frontend/src/app/5_pages/MarketMakingPage/components/AdjustAndDepositModal/AdjustAndDepositModal.tsx @@ -20,6 +20,8 @@ import { } from '@sovryn/ui'; import { Decimal } from '@sovryn/utils'; +import { RSK_CHAIN_ID } from '../../../../../config/chains'; + import { AssetRenderer } from '../../../../2_molecules/AssetRenderer/AssetRenderer'; import { CurrentStatistics } from '../../../../2_molecules/CurrentStatistics/CurrentStatistics'; import { LabelWithTabsAndMaxButton } from '../../../../2_molecules/LabelWithTabsAndMaxButton/LabelWithTabsAndMaxButton'; @@ -27,7 +29,7 @@ import { MaxButton } from '../../../../2_molecules/MaxButton/MaxButton'; import { useAccount } from '../../../../../hooks/useAccount'; import { useWeiAmountInput } from '../../../../../hooks/useWeiAmountInput'; import { translations } from '../../../../../locales/i18n'; -import { COMMON_SYMBOLS } from '../../../../../utils/asset'; +import { COMMON_SYMBOLS, findAsset } from '../../../../../utils/asset'; import { decimalic, toWei } from '../../../../../utils/math'; import { useCheckPoolBlocked } from '../../hooks/useCheckPoolBlocked'; import { useGetExpectedTokenAmount } from '../../hooks/useGetExpectedTokenAmount'; @@ -63,7 +65,13 @@ export const AdjustAndDepositModal: FC = ({ }) => { const [adjustType, setAdjustType] = useState(AdjustType.Deposit); const [selectedAsset, setSelectedAsset] = useState(pool.assetA); - const [value, setValue, amount] = useWeiAmountInput(''); + // Parse the input with the asset's real decimals so `amount` is in the + // token's native units (USDT0 has 6 decimals, other AMM tokens 18). + const tokenDecimals = useMemo( + () => findAsset(selectedAsset, RSK_CHAIN_ID)?.decimals ?? 18, + [selectedAsset], + ); + const [value, setValue, amount] = useWeiAmountInput('', tokenDecimals); const { account } = useAccount(); const poolBlocked = useCheckPoolBlocked(pool); @@ -135,8 +143,10 @@ export const AdjustAndDepositModal: FC = ({ account, ); + const decimalValue = useMemo(() => decimalic(value), [value]); + const poolWeiAmount = calculatePoolWeiAmount( - Decimal.fromBigNumberString(amount.toString()), + decimalValue, selectedAsset === pool.assetA ? balanceA : balanceB, tokenPoolBalance, ); @@ -178,12 +188,16 @@ export const AdjustAndDepositModal: FC = ({ [isDeposit, maxTokenToDepositAmount, maxWithdrawalAmount], ); - const handleMaxClick = useCallback( - () => setValue(maxBalance.toString()), - [maxBalance, setValue], - ); - - const decimalValue = useMemo(() => decimalic(value), [value]); + const handleMaxClick = useCallback(() => { + // Floor to the asset's decimals so the value stays parseable in the + // token's native units (e.g. USDT0 supports only 6 decimal places). + const [integer, fraction] = maxBalance.toString().split('.'); + setValue( + fraction && fraction.length > tokenDecimals + ? `${integer}.${fraction.slice(0, tokenDecimals)}` + : maxBalance.toString(), + ); + }, [maxBalance, setValue, tokenDecimals]); const expectedTokenAmount = useGetExpectedTokenAmount(pool, decimalValue); const minReturn1 = getMinReturn(decimalAmount); @@ -339,6 +353,7 @@ export const AdjustAndDepositModal: FC = ({ value={value} onChangeText={setValue} maxAmount={maxBalance.toNumber()} + decimalPrecision={tokenDecimals} label={t(translations.common.amount)} className="max-w-none" unit={} diff --git a/apps/frontend/src/app/5_pages/MarketMakingPage/components/AdjustAndDepositModal/components/NewPoolStatistics/NewPoolStatistics.tsx b/apps/frontend/src/app/5_pages/MarketMakingPage/components/AdjustAndDepositModal/components/NewPoolStatistics/NewPoolStatistics.tsx index 814af611a..e708675fe 100644 --- a/apps/frontend/src/app/5_pages/MarketMakingPage/components/AdjustAndDepositModal/components/NewPoolStatistics/NewPoolStatistics.tsx +++ b/apps/frontend/src/app/5_pages/MarketMakingPage/components/AdjustAndDepositModal/components/NewPoolStatistics/NewPoolStatistics.tsx @@ -82,7 +82,7 @@ export const NewPoolStatistics: FC = ({ return adjustType === AdjustType.Deposit || isInitialDeposit ? balanceB.add(expectedTokenAmount) - : decimalAmount.eq(balanceA) + : decimalValue.eq(balanceA) ? Decimal.ZERO : balanceB.sub(expectedTokenAmount); }, [ @@ -92,7 +92,6 @@ export const NewPoolStatistics: FC = ({ isInitialDeposit, balanceB, expectedTokenAmount, - decimalAmount, balanceA, isDeposit, decimalValue, diff --git a/apps/frontend/src/app/5_pages/MarketMakingPage/components/AdjustAndDepositModal/hooks/useGetPoolsBalance.ts b/apps/frontend/src/app/5_pages/MarketMakingPage/components/AdjustAndDepositModal/hooks/useGetPoolsBalance.ts index be46f5ffe..7c579a4d3 100644 --- a/apps/frontend/src/app/5_pages/MarketMakingPage/components/AdjustAndDepositModal/hooks/useGetPoolsBalance.ts +++ b/apps/frontend/src/app/5_pages/MarketMakingPage/components/AdjustAndDepositModal/hooks/useGetPoolsBalance.ts @@ -10,6 +10,7 @@ import { RSK_CHAIN_ID } from '../../../../../../config/chains'; import { asyncCall } from '../../../../../../store/rxjs/provider-cache'; import { COMMON_SYMBOLS } from '../../../../../../utils/asset'; +import { decimalic, fromWei } from '../../../../../../utils/math'; import { AmmLiquidityPool } from '../../../utils/AmmLiquidityPool'; export const useGetPoolsBalance = (pool: AmmLiquidityPool) => { @@ -39,7 +40,12 @@ export const useGetPoolsBalance = (pool: AmmLiquidityPool) => { asyncCall( `loanToken/${loanTokenContract.address}/balanceOf/${pool.converter}`, () => - contractA.balanceOf(pool.converter).then(Decimal.fromBigNumberString), + contractA + .balanceOf(pool.converter) + // Asset A may not use 18 decimals (e.g. USDT0 has 6). + .then(balance => + decimalic(fromWei(balance, loanTokenContract.decimals)), + ), ), asyncCall( `loanToken/${wrbtcContract.address}/balanceOf/${pool.converter}`, diff --git a/apps/frontend/src/app/5_pages/MarketMakingPage/hooks/useGetPoolLiquidity.ts b/apps/frontend/src/app/5_pages/MarketMakingPage/hooks/useGetPoolLiquidity.ts index c4d98027e..0934a0835 100644 --- a/apps/frontend/src/app/5_pages/MarketMakingPage/hooks/useGetPoolLiquidity.ts +++ b/apps/frontend/src/app/5_pages/MarketMakingPage/hooks/useGetPoolLiquidity.ts @@ -9,7 +9,8 @@ import { RSK_CHAIN_ID } from '../../../../config/chains'; import { useGetTokenContract } from '../../../../hooks/useGetContract'; import { asyncCall } from '../../../../store/rxjs/provider-cache'; -import { COMMON_SYMBOLS } from '../../../../utils/asset'; +import { COMMON_SYMBOLS, findAsset } from '../../../../utils/asset'; +import { decimalic, fromWei } from '../../../../utils/math'; import { AmmLiquidityPool } from '../utils/AmmLiquidityPool'; export const useGetPoolLiquidity = (pool: AmmLiquidityPool) => { @@ -31,13 +32,22 @@ export const useGetPoolLiquidity = (pool: AmmLiquidityPool) => { } try { - const fetchBalance = async (contract: Contract, type: string) => + // Asset A may not use 18 decimals (e.g. USDT0 has 6). + const fetchBalance = async ( + contract: Contract, + type: string, + decimals: number = 18, + ) => await asyncCall(`${type}/balanceOf/${pool.converter}`, () => contract.balanceOf(pool.converter), - ).then(Decimal.fromBigNumberString); + ).then(balance => decimalic(fromWei(balance, decimals))); const [tokenBalance, btcBalance] = await Promise.all([ - fetchBalance(contractTokenA, pool.assetA), + fetchBalance( + contractTokenA, + pool.assetA, + findAsset(pool.assetA, RSK_CHAIN_ID)?.decimals ?? 18, + ), fetchBalance(contractTokenB, COMMON_SYMBOLS.WBTC), ]); @@ -62,7 +72,11 @@ export const useGetPoolLiquidity = (pool: AmmLiquidityPool) => { ); try { - const fetchBalance = async (tokenContract: Contract) => + // Asset A may not use 18 decimals (e.g. USDT0 has 6). + const fetchBalance = async ( + tokenContract: Contract, + decimals: number = 18, + ) => await asyncCall( `${ pool.converter @@ -71,10 +85,13 @@ export const useGetPoolLiquidity = (pool: AmmLiquidityPool) => { contract.reserveStakedBalance( tokenContract.address.toLowerCase(), ), - ).then(Decimal.fromBigNumberString); + ).then(balance => decimalic(fromWei(balance, decimals))); const [tokenBalance, btcBalance] = await Promise.all([ - fetchBalance(contractTokenA), + fetchBalance( + contractTokenA, + findAsset(pool.assetA, RSK_CHAIN_ID)?.decimals ?? 18, + ), fetchBalance(contractTokenB), ]); diff --git a/apps/frontend/src/app/5_pages/MarketMakingPage/hooks/useGetUserInfo.ts b/apps/frontend/src/app/5_pages/MarketMakingPage/hooks/useGetUserInfo.ts index d2c30431c..af17ac0e1 100644 --- a/apps/frontend/src/app/5_pages/MarketMakingPage/hooks/useGetUserInfo.ts +++ b/apps/frontend/src/app/5_pages/MarketMakingPage/hooks/useGetUserInfo.ts @@ -14,7 +14,8 @@ import { useGetProtocolContract, useGetTokenContract, } from '../../../../hooks/useGetContract'; -import { COMMON_SYMBOLS } from '../../../../utils/asset'; +import { COMMON_SYMBOLS, findAsset } from '../../../../utils/asset'; +import { decimalic, fromWei } from '../../../../utils/math'; import { AmmLiquidityPool } from '../utils/AmmLiquidityPool'; export const useGetUserInfo = (pool: AmmLiquidityPool) => { @@ -79,9 +80,12 @@ export const useGetUserInfo = (pool: AmmLiquidityPool) => { const totalSupply = await contract .totalSupply() .then(Decimal.fromBigNumberString); + // Asset A may not use 18 decimals (e.g. USDT0 has 6), so convert its + // raw balance with the asset's real decimals. + const decimalsA = findAsset(assetA, chainId)?.decimals ?? 18; const converterBalanceA = await contractTokenA ?.balanceOf(pool.converter) - .then(Decimal.fromBigNumberString); + .then(balance => decimalic(fromWei(balance, decimalsA))); const converterBalanceB = await contractTokenB ?.balanceOf(pool.converter) .then(Decimal.fromBigNumberString); @@ -137,6 +141,8 @@ export const useGetUserInfo = (pool: AmmLiquidityPool) => { account, pool.converterVersion, pool.converter, + assetA, + chainId, contractTokenA, contractTokenB, poolTokenB, diff --git a/packages/sdk/src/_tests/swaps/smart-router.test.ts b/packages/sdk/src/_tests/swaps/smart-router.test.ts index 594e0ed3f..00e5759bf 100644 --- a/packages/sdk/src/_tests/swaps/smart-router.test.ts +++ b/packages/sdk/src/_tests/swaps/smart-router.test.ts @@ -117,12 +117,12 @@ describe('SmartRouter', () => { }); it('returns all available entries', async () => { - await expect(router.getEntries(chainId)).resolves.toHaveLength(17); + await expect(router.getEntries(chainId)).resolves.toHaveLength(18); }); it('returns all available destinations for entry token', async () => { await expect(router.getDestination(chainId, sov)).resolves.toHaveLength( - 15, + 16, ); }); diff --git a/packages/sdk/src/constants.ts b/packages/sdk/src/constants.ts index ebd9630af..1eaa73547 100644 --- a/packages/sdk/src/constants.ts +++ b/packages/sdk/src/constants.ts @@ -7,4 +7,5 @@ export const RSK_STABLECOINS: string[] = [ 'XUSD', 'DOC', 'RUSDT', + 'USDT0', ]; diff --git a/packages/sdk/src/swaps/smart-router/routes/amm-swap-route.ts b/packages/sdk/src/swaps/smart-router/routes/amm-swap-route.ts index af3cec8f2..aaedf3481 100644 --- a/packages/sdk/src/swaps/smart-router/routes/amm-swap-route.ts +++ b/packages/sdk/src/swaps/smart-router/routes/amm-swap-route.ts @@ -1,6 +1,16 @@ -import { BigNumber, Contract, constants, providers } from 'ethers'; +import { + BigNumber, + BigNumberish, + Contract, + constants, + providers, +} from 'ethers'; -import { getAssetContract, getProtocolContract } from '@sovryn/contracts'; +import { + getAssetContract, + getAssetDataByAddress, + getProtocolContract, +} from '@sovryn/contracts'; import { ChainId, ChainIds, numberToChainId } from '@sovryn/ethers-provider'; import { RSK_STABLECOINS } from '../../../constants'; @@ -25,6 +35,8 @@ export const ammSwapRoute: SwapRouteFunction = ( let protocolContract: Contract; let wrbtcMinter: Contract; + const decimalsCache: Record = {}; + const getChainId = async () => { if (!chainId) { chainId = numberToChainId((await provider.getNetwork()).chainId); @@ -101,6 +113,47 @@ export const ammSwapRoute: SwapRouteFunction = ( return token; }; + // The smart router works with 18-decimal-normalized amounts, while the AMM + // converters operate in each token's native units. Every RSK AMM token used + // to be 18 decimals, but USDT0 (6 decimals) breaks that assumption, so we + // convert amounts in/out of native units around every on-chain call. These + // helpers are a no-op for 18-decimal tokens. + const getTokenDecimals = async (token: string): Promise => { + if (await isNativeToken(token)) { + return 18; + } + const address = token.toLowerCase(); + if (decimalsCache[address] === undefined) { + const chainId = await getChainId(); + // Tokens missing from the asset registry keep the legacy 18-decimals + // pass-through so unsupported pairs still fail at the contract level. + decimalsCache[address] = await getAssetDataByAddress(token, chainId) + .then(item => item.decimals) + .catch(() => 18); + } + return decimalsCache[address]; + }; + + // 18-decimal-normalized -> token native units + const denormalizeAmount = async (token: string, amount: BigNumberish) => { + const decimals = await getTokenDecimals(token); + const value = BigNumber.from(amount); + if (decimals === 18) { + return value; + } + return value.div(BigNumber.from(10).pow(18 - decimals)); + }; + + // token native units -> 18-decimal-normalized + const normalizeAmount = async (token: string, amount: BigNumberish) => { + const decimals = await getTokenDecimals(token); + const value = BigNumber.from(amount); + if (decimals === 18) { + return value; + } + return value.mul(BigNumber.from(10).pow(18 - decimals)); + }; + return { name: 'AMM', chains: [ChainIds.RSK_MAINNET, ChainIds.RSK_TESTNET], @@ -125,6 +178,7 @@ export const ammSwapRoute: SwapRouteFunction = ( 'BPRO', 'POWA', 'BOS', + 'USDT0', ]; const contracts = ( @@ -171,11 +225,17 @@ export const ammSwapRoute: SwapRouteFunction = ( const baseToken = await getTokenAddress(entry); const quoteToken = await getTokenAddress(destination); - return (await getSwapQuoteContract()) - .getSwapExpectedReturn(baseToken, quoteToken, amount) + + // Quote in native units, return normalized to 18 decimals. + const entryAmount = await denormalizeAmount(entry, amount); + + const expectedReturn = await (await getSwapQuoteContract()) + .getSwapExpectedReturn(baseToken, quoteToken, entryAmount) .catch(e => { throw makeError(e.message, SovrynErrorCode.ETHERS_CALL_EXCEPTION); }); + + return normalizeAmount(destination, expectedReturn); }, approve: async (entry, destination, amount, from, overrides) => { // native token is always approved @@ -193,24 +253,26 @@ export const ammSwapRoute: SwapRouteFunction = ( const converter = await getConverterContract(entry, destination); + // The incoming amount is 18-decimal-normalized; approve in native units. + const approveAmount = + amount === undefined || amount === null + ? constants.MaxUint256 + : await denormalizeAmount(entry, amount); + if ( await hasEnoughAllowance( provider, entry, from, converter.address, - amount ?? constants.MaxUint256, + approveAmount, ) ) { return undefined; } return { - ...makeApproveRequest( - entry, - converter.address, - amount ?? constants.MaxUint256, - ), + ...makeApproveRequest(entry, converter.address, approveAmount), ...overrides, }; }, @@ -265,6 +327,11 @@ export const ammSwapRoute: SwapRouteFunction = ( const converter = await getConverterContract(entry, destination); + // `quote()` returns an 18-decimal-normalized amount; convert both the + // input amount and the min return into the tokens' native units for the + // on-chain call. + const entryAmount = await denormalizeAmount(entry, amount); + const expectedReturn = await this.quote( entry, destination, @@ -272,14 +339,17 @@ export const ammSwapRoute: SwapRouteFunction = ( options, ); - const minReturn = getMinReturn(expectedReturn, options?.slippage); + const minReturn = await denormalizeAmount( + destination, + getMinReturn(expectedReturn, options?.slippage), + ); - let args = [path, amount, minReturn]; + let args = [path, entryAmount, minReturn]; if (!entryIsNative && !destinationIsNative) { args = [ path, - amount, + entryAmount, minReturn, constants.AddressZero, constants.AddressZero, @@ -290,7 +360,7 @@ export const ammSwapRoute: SwapRouteFunction = ( return { to: converter.address, data: converter.interface.encodeFunctionData('convertByPath', args), - value: entryIsNative ? amount.toString() : '0', + value: entryIsNative ? entryAmount.toString() : '0', ...overrides, }; }, From ad873434a1999b067cd39d64abeef2529cd49913 Mon Sep 17 00:00:00 2001 From: Tyrone Johnson Date: Wed, 8 Jul 2026 15:48:17 +0300 Subject: [PATCH 4/5] chore: changeset for sdk USDT0 decimals fix --- .changeset/shiny-poets-swap.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shiny-poets-swap.md diff --git a/.changeset/shiny-poets-swap.md b/.changeset/shiny-poets-swap.md new file mode 100644 index 000000000..f6581baf4 --- /dev/null +++ b/.changeset/shiny-poets-swap.md @@ -0,0 +1,5 @@ +--- +"@sovryn/sdk": patch +--- + +Support non-18-decimal tokens (USDT0) in the RSK AMM swap route: add USDT0 to the tradeable token list and normalize amounts between the router's 18-decimal convention and each token's native units in quote/swap/approve From 42abdd12308b5062b80e51618a2e3a60e49c7322 Mon Sep 17 00:00:00 2001 From: Tyrone Johnson Date: Wed, 8 Jul 2026 16:01:39 +0300 Subject: [PATCH 5/5] revert tech README.md update - remove a dangling line --- apps/frontend/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/frontend/README.md b/apps/frontend/README.md index 2b0cc4038..a3358b480 100644 --- a/apps/frontend/README.md +++ b/apps/frontend/README.md @@ -15,4 +15,3 @@ yarn --cwd apps/frontend serve To use custom environment variables locally, create a file named `.env.local` and add required values (see `.env.example` for sample variables and values). For environment variables used in deployments see `netlify.toml`. -