Skip to content

Commit

Permalink
feat: allow WRBTC to be traded on smart router (#960)
Browse files Browse the repository at this point in the history
  • Loading branch information
creed-victor authored Jul 23, 2024
1 parent 3d906cb commit c7b7d38
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 23 deletions.
8 changes: 8 additions & 0 deletions .changeset/little-rockets-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'frontend': patch
'@sovryn/contracts': patch
'@sovryn/sdex': patch
'@sovryn/sdk': patch
---

SOV-4286: allow WRBTC to be traded on smart router
16 changes: 14 additions & 2 deletions apps/frontend/src/app/5_pages/ConvertPage/ConvertPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import {
} from '@sovryn/ui';
import { Decimal } from '@sovryn/utils';

import { RSK_CHAIN_ID } from '../../../config/chains';

import { AmountRenderer } from '../../2_molecules/AmountRenderer/AmountRenderer';
import { AssetRenderer } from '../../2_molecules/AssetRenderer/AssetRenderer';
import { MaxButton } from '../../2_molecules/MaxButton/MaxButton';
Expand All @@ -42,7 +44,11 @@ import { useAssetBalance } from '../../../hooks/useAssetBalance';
import { useCurrentChain } from '../../../hooks/useChainStore';
import { useWeiAmountInput } from '../../../hooks/useWeiAmountInput';
import { translations } from '../../../locales/i18n';
import { COMMON_SYMBOLS, listAssetsOfChain } from '../../../utils/asset';
import {
COMMON_SYMBOLS,
findAsset,
listAssetsOfChain,
} from '../../../utils/asset';
import { removeTrailingZerosFromString } from '../../../utils/helpers';
import { decimalic, fromWei } from '../../../utils/math';
import { FIXED_MYNT_RATE, FIXED_RATE_ROUTES } from './ConvertPage.constants';
Expand Down Expand Up @@ -77,7 +83,13 @@ const ConvertPage: FC = () => {
callback: (options: SelectOption<string>[]) => void,
) =>
Promise.all(
addresses.map(address => smartRouter.getTokenDetails(address, chain)),
addresses
.filter(
// filter out WBTC token on rsk chain
item =>
findAsset('WBTC', RSK_CHAIN_ID).address !== item.toLowerCase(),
)
.map(address => smartRouter.getTokenDetails(address, chain)),
).then(tokens =>
callback(
tokens.map(token => ({
Expand Down
26 changes: 26 additions & 0 deletions packages/contracts/src/abis/wrbtc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[
{
"constant": false,
"inputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "withdraw",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "deposit",
"outputs": [],
"payable": true,
"stateMutability": "payable",
"type": "function"
}
]
4 changes: 4 additions & 0 deletions packages/contracts/src/contracts/protocol/rsk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export const rsk: Record<string, AsyncContractConfigData> = {
getAbi: async () =>
(await import('../../abis/btcWrapperProxy.json')).default,
},
wrbtcMinter: {
address: '0x542fDA317318eBF1d3DEAf76E0b632741A7e677d',
getAbi: async () => (await import('../../abis/wrbtc.json')).default,
},
massetManager: {
address: '0x5F777270259E32F79589fe82269DB6209F7b7582',
getAbi: async () => (await import('../../abis/massetManager.json')).default,
Expand Down
4 changes: 4 additions & 0 deletions packages/contracts/src/contracts/protocol/rskTestnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export const rskTestnet: Record<string, AsyncContractConfigData> = {
getAbi: async () =>
(await import('../../abis/btcWrapperProxy.json')).default,
},
wrbtcMinter: {
address: '0x69FE5cEC81D5eF92600c1A0dB1F11986AB3758Ab',
getAbi: async () => (await import('../../abis/wrbtc.json')).default,
},
massetManager: {
address: '0xac2d05A148aB512EDEDc7280c00292ED33d31f1A',
getAbi: async () => (await import('../../abis/massetManager.json')).default,
Expand Down
26 changes: 19 additions & 7 deletions packages/sdex/src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import { datadogLogs } from '@datadog/browser-logs';

datadogLogs.init({
clientToken: process.env.REACT_APP_DATADOG_CLIENT_TOKEN || '',
site: 'datadoghq.com',
forwardErrorsToLogs: true,
sessionSampleRate: 100,
service: 'd2',
});
if (
process.env.REACT_APP_DATADOG_CLIENT_TOKEN &&
process.env.NODE_ENV === 'production'
) {
datadogLogs.init({
clientToken: process.env.REACT_APP_DATADOG_CLIENT_TOKEN || '',
site: 'datadoghq.com',
forwardErrorsToLogs: true,
sessionSampleRate: 100,
service: 'd2',
});
}

export const logger = (type: 'info' | 'error', title: string, data: object) => {
if (
!process.env.REACT_APP_DATADOG_CLIENT_TOKEN ||
process.env.NODE_ENV !== 'production'
) {
console[type](title, data);
return;
}
if (type === 'error') {
datadogLogs.logger.error(title, data);
} else if (type === 'info') {
Expand Down
83 changes: 82 additions & 1 deletion packages/sdk/src/_tests/swaps/routes/amm-swap-route.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { BigNumber, constants } from 'ethers';
import { BigNumber, constants, Contract, ethers } from 'ethers';
import { parseUnits } from 'ethers/lib/utils';

import { getProtocolContract } from '@sovryn/contracts';

import { ammSwapRoute } from '../../../swaps/smart-router/routes/amm-swap-route';
import { SwapRoute } from '../../../swaps/smart-router/types';
import { makeChainFixture } from '../../_fixtures/chain';
Expand All @@ -14,6 +16,9 @@ describe('AMM Route', () => {
let sov: string;
let rbtc: string;
let dllr: string;
let wbtc: string;
let swapNetwork: Contract;
let wbtcProxy: Contract;

const AddressOne = '0x0000000000000000000000000000000000000001';

Expand All @@ -23,6 +28,9 @@ describe('AMM Route', () => {
sov = await makeTokenAddress('SOV');
dllr = await makeTokenAddress('DLLR');
rbtc = await makeTokenAddress('BTC');
wbtc = await makeTokenAddress('WBTC');
swapNetwork = (await getProtocolContract('swapNetwork')).contract();
wbtcProxy = (await getProtocolContract('btcWrapperProxy')).contract();
});

it('has correct name', () => {
Expand Down Expand Up @@ -139,5 +147,78 @@ describe('AMM Route', () => {
value: '0',
});
});

it('BTC -> WBTC uses WBTC contract to mint tokens.', async () => {
const amount = parseUnits('0.01');
await expect(
route.swap(rbtc, wbtc, amount, constants.AddressZero),
).resolves.toMatchObject({
to: wbtc,
data: '0xd0e30db0', // deposit()
value: amount.toString(),
});
});

it('WBTC -> BTC uses WBTC contract to burn tokens.', async () => {
const amount = parseUnits('0.01');
const iface = new ethers.utils.Interface([
'function withdraw(uint256) external',
]);
await expect(
route.swap(wbtc, rbtc, amount, constants.AddressZero),
).resolves.toMatchObject({
to: wbtc,
data: iface.encodeFunctionData('withdraw', [amount]),
});
});

it('BTC -> SOV uses WBTC proxy to swap.', async () => {
const amount = parseUnits('0.01');
await expect(
route.swap(rbtc, sov, amount, constants.AddressZero),
).resolves.toMatchObject({
to: wbtcProxy.address,
data: expect.stringContaining(
wbtcProxy.interface.getSighash('convertByPath'),
),
value: amount.toString(),
});
});

it('SOV -> BTC uses WBTC proxy to swap.', async () => {
const amount = parseUnits('0.01');
await expect(
route.swap(sov, rbtc, amount, constants.AddressZero),
).resolves.toMatchObject({
to: wbtcProxy.address,
data: expect.stringContaining(
wbtcProxy.interface.getSighash('convertByPath'),
),
});
});

it('WBTC -> SOV uses swap network to swap.', async () => {
const amount = parseUnits('0.01');
await expect(
route.swap(wbtc, sov, amount, constants.AddressZero),
).resolves.toMatchObject({
to: swapNetwork.address,
data: expect.stringContaining(
swapNetwork.interface.getSighash('convertByPath'),
),
});
});

it('SOV -> WBTC uses swap network to swap.', async () => {
const amount = parseUnits('0.01');
await expect(
route.swap(sov, wbtc, amount, constants.AddressZero),
).resolves.toMatchObject({
to: swapNetwork.address,
data: expect.stringContaining(
swapNetwork.interface.getSighash('convertByPath'),
),
});
});
});
});
4 changes: 2 additions & 2 deletions packages/sdk/src/_tests/swaps/smart-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,12 @@ describe('SmartRouter', () => {
});

it('returns all available entries', async () => {
await expect(router.getEntries(chainId)).resolves.toHaveLength(15);
await expect(router.getEntries(chainId)).resolves.toHaveLength(16);
});

it('returns all available destinations for entry token', async () => {
await expect(router.getDestination(chainId, sov)).resolves.toHaveLength(
13,
14,
);
});

Expand Down
80 changes: 69 additions & 11 deletions packages/sdk/src/swaps/smart-router/routes/amm-swap-route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Contract, constants, providers } from 'ethers';
import { BigNumber, Contract, constants, providers } from 'ethers';

import { getAssetContract, getProtocolContract } from '@sovryn/contracts';
import { ChainId, ChainIds, numberToChainId } from '@sovryn/ethers-provider';
Expand All @@ -23,6 +23,7 @@ export const ammSwapRoute: SwapRouteFunction = (
let swapConverter: Contract;
let rbtcConverter: Contract;
let protocolContract: Contract;
let wrbtcMinter: Contract;

const getChainId = async () => {
if (!chainId) {
Expand Down Expand Up @@ -68,15 +69,26 @@ export const ammSwapRoute: SwapRouteFunction = (
return protocolContract;
};

const getWrbtcMinter = async () => {
if (!wrbtcMinter) {
const chainId = await getChainId();
const { address, abi } = await getProtocolContract(
'wrbtcMinter',
chainId,
);
wrbtcMinter = new Contract(address, abi, provider);
}
return wrbtcMinter;
};

const isNativeToken = async (token: string) =>
token === constants.AddressZero ||
token === constants.AddressZero;

const isNativeWrapper = async (token: string) =>
token ===
(
await getAssetContract('WBTC', await getChainId())
).address.toLowerCase();
(await getAssetContract('WBTC', await getChainId())).address.toLowerCase();

const validatedTokenAddress = async (token: string) => {
token = token.toLowerCase();
const getTokenAddress = async (token: string) => {
if (await isNativeToken(token)) {
if (wrbtcAddress) {
return wrbtcAddress;
Expand All @@ -98,6 +110,7 @@ export const ammSwapRoute: SwapRouteFunction = (

const swapTokens = [
'BTC',
'WBTC',
'DLLR',
'FISH',
'MOC',
Expand Down Expand Up @@ -147,8 +160,16 @@ export const ammSwapRoute: SwapRouteFunction = (
return pairCache;
},
quote: async (entry, destination, amount) => {
const baseToken = await validatedTokenAddress(entry);
const quoteToken = await validatedTokenAddress(destination);
if (
((await isNativeToken(entry)) &&
(await isNativeWrapper(destination))) ||
((await isNativeToken(destination)) && (await isNativeWrapper(entry)))
) {
return BigNumber.from(amount);
}

const baseToken = await getTokenAddress(entry);
const quoteToken = await getTokenAddress(destination);
return (await getSwapQuoteContract())
.getSwapExpectedReturn(baseToken, quoteToken, amount)
.catch(e => {
Expand All @@ -161,6 +182,14 @@ export const ammSwapRoute: SwapRouteFunction = (
return undefined;
}

// swapping from WRBTC to RBTC is always approved
if (
(await isNativeWrapper(entry)) &&
(await isNativeToken(destination))
) {
return undefined;
}

const converter = await getConverterContract(entry, destination);

if (
Expand Down Expand Up @@ -194,8 +223,37 @@ export const ammSwapRoute: SwapRouteFunction = (
);
}

const baseToken = await validatedTokenAddress(entry);
const quoteToken = await validatedTokenAddress(destination);
// RBTC -> WRBTC
if (
(await isNativeToken(entry)) &&
(await isNativeWrapper(destination))
) {
const minter = await getWrbtcMinter();
return {
to: minter.address,
data: minter.interface.encodeFunctionData('deposit'),
value: amount.toString(),
gasLimit: 30_000,
...overrides,
};
}

// WRBTC -> RBTC
if (
(await isNativeWrapper(entry)) &&
(await isNativeToken(destination))
) {
const minter = await getWrbtcMinter();
return {
to: minter.address,
data: minter.interface.encodeFunctionData('withdraw', [amount]),
// gasLimit: 30_000,
...overrides,
};
}

const baseToken = await getTokenAddress(entry);
const quoteToken = await getTokenAddress(destination);

const entryIsNative = await isNativeToken(entry);
const destinationIsNative = await isNativeToken(destination);
Expand Down

0 comments on commit c7b7d38

Please sign in to comment.