Skip to content

Commit

Permalink
[TP-1811] Always call refundETH for native token-in swaps (#1267)
Browse files Browse the repository at this point in the history
Co-authored-by: Pano Skylakis <[email protected]>
  • Loading branch information
pano-skylakis and Pano Skylakis authored Dec 8, 2023
1 parent 37a46ba commit 2212b95
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 25 deletions.
41 changes: 20 additions & 21 deletions packages/internal/dex/sdk-sample-app/src/components/Example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const allTokens: Token[] = [
{ symbol: 'zkSRE', address: '0x43566cAB87CC147C95e2895E7b972E19993520e4' },
{ symbol: 'zkCORE', address: '0x4B96E7b7eA673A996F140d5De411a97b7eab934E' },
{ symbol: 'zkWAT', address: '0xaC953a0d7B67Fae17c87abf79f09D0f818AC66A2' },
{ symbol: 'zkCATS', address: '0xb95B75B4E4c09F04d5DA6349861BF1b6F163D78c' },
{ symbol: 'zkYEET', address: '0x8AC26EfCbf5D700b37A27aA00E6934e6904e7B8e' },
];

const buildExchange = (secondaryFeeRecipient: string, secondaryFeePercentage: number) => {
Expand Down Expand Up @@ -109,7 +111,7 @@ export function Example() {
const performSwap = async (result: TransactionResponse) => {
setSwapTransaction(null);
setIsFetching(true);
const provider = new ethers.providers.Web3Provider((window as any).ethereum)
const provider = new ethers.providers.Web3Provider((window as any).ethereum);
const signer = provider.getSigner();

// Approve the ERC20 spend
Expand Down Expand Up @@ -157,13 +159,13 @@ export function Example() {
</div>
<div>
<select
className='dark:bg-slate-800'
value={tradeType}
onChange={(e) => {
setTradeType(e.target.value as TradeType);
setResult(null);
setSwapTransaction(null)
}}
>
setSwapTransaction(null);
}}>
<option>exactInput</option>
<option>exactOutput</option>
</select>
Expand All @@ -176,16 +178,16 @@ export function Example() {
</div>
<div>
<select
className='dark:bg-slate-800'
value={inputToken.address}
onChange={(e) => {
setInputToken({
address: e.target.value,
symbol: addressToSymbolMapping[e.target.value],
});
setResult(null);
setSwapTransaction(null)
}}
>
setSwapTransaction(null);
}}>
{allTokens.map((token) => (
<option key={token.address} value={token.address}>
{token.symbol}
Expand All @@ -201,16 +203,16 @@ export function Example() {
</div>
<div>
<select
className='dark:bg-slate-800'
value={outputToken.address}
onChange={(e) => {
setOutputToken({
address: e.target.value,
symbol: addressToSymbolMapping[e.target.value],
});
setResult(null);
setSwapTransaction(null)
}}
>
setSwapTransaction(null);
}}>
{allTokens.map((token) => (
<option key={token.address} value={token.address}>
{token.symbol}
Expand All @@ -220,7 +222,7 @@ export function Example() {
</div>
</div>

<hr className="my-4" />
<hr className='my-4' />

<SecondaryFeeInput setSecondaryFeeRecipient={setSecondaryFeeRecipient} setFeePercentage={setFeePercentage} />
{tradeType === 'exactInput' && inputToken && (
Expand All @@ -232,15 +234,14 @@ export function Example() {

{inputToken && outputToken && (
<button
className="disabled:opacity-50 mt-2 py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75"
className='disabled:opacity-50 mt-2 py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75'
onClick={() => getQuote(inputToken.address, outputToken.address)}
disabled={isFetching}
>
disabled={isFetching}>
Get Quote
</button>
)}

<hr className="my-4" />
<hr className='my-4' />
{error && <ErrorMessage message={error} />}
{result && (
<>
Expand All @@ -262,10 +263,9 @@ export function Example() {

<>
<button
className="disabled:opacity-50 mt-2 py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75"
className='disabled:opacity-50 mt-2 py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75'
onClick={() => performSwap(result)}
disabled={isFetching}
>
disabled={isFetching}>
{approved ? 'Swap' : 'Approve'}
</button>
{isFetching && <h3>loading...</h3>}
Expand All @@ -275,10 +275,9 @@ export function Example() {
Swap successful! Check your metamask to see updated token balances
</h3>
<a
className="underline text-blue-600 hover:text-blue-800 visited:text-purple-600"
className='underline text-blue-600 hover:text-blue-800 visited:text-purple-600'
href={`https://explorer.testnet.immutable.com/tx/${swapTransaction.transactionHash}`}
target="_blank"
>
target='_blank'>
Transaction
</a>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
expectToBeString,
decodeMulticallExactInputWithoutFees,
buildBlock,
refundETHFunctionSignature,
TEST_MAX_PRIORITY_FEE_PER_GAS,
TEST_BASE_FEE,
} from './test/utils';
Expand Down Expand Up @@ -433,6 +434,44 @@ describe('getUnsignedSwapTxFromAmountIn', () => {

expect(result.approval).toBeNull();
});

it('should include a call to refundETH as the final step of the multicall calldata', async () => {
mockRouterImplementation({
pools: [createPool(nativeTokenService.wrappedToken, FUN_TEST_TOKEN)],
});

const swapRouterInterface = SwapRouter.INTERFACE;
const paymentsInterface = PaymentsExtended.INTERFACE;
const exchange = new Exchange(TEST_DEX_CONFIGURATION);

// Sell 100 native tokens for X amount of FUN where the exchange rate is 1 token-in : 10 token-out
// Route is WIMX > FUN
const { swap } = await exchange.getUnsignedSwapTxFromAmountIn(
TEST_FROM_ADDRESS,
'native',
FUN_TEST_TOKEN.address,
newAmountFromString('100', nativeTokenService.nativeToken).value,
3, // 3 % slippage
);

expectToBeDefined(swap.transaction.data);
expectToBeDefined(swap.transaction.value);
const calldata = swap.transaction.data.toString();

const topLevelParams = swapRouterInterface.decodeFunctionData('multicall(uint256,bytes[])', calldata);

expect(topLevelParams.data.length).toBe(2); // expect that there are two calls in the multicall
const swapTransactionCalldata = topLevelParams.data[0];
const refundETHTransactionCalldata = topLevelParams.data[1];

expectToBeString(swapTransactionCalldata);
expectToBeString(refundETHTransactionCalldata);

const decodedRefundEthTx = paymentsInterface.decodeFunctionData('refundETH', refundETHTransactionCalldata);

expect(topLevelParams.data[1]).toEqual(refundETHFunctionSignature);
expect(decodedRefundEthTx.length).toEqual(0); // expect that the refundETH call has no parameters
});
});

describe('with a single pool and a native token out', () => {
Expand Down Expand Up @@ -583,6 +622,49 @@ describe('getUnsignedSwapTxFromAmountIn', () => {
});

describe('with multiple pools', () => {
describe('with a native token in', () => {
it('should include a call to refundETH as the final step of the multicall calldata', async () => {
mockRouterImplementation({
pools: [
createPool(nativeTokenService.wrappedToken, USDC_TEST_TOKEN),
createPool(USDC_TEST_TOKEN, FUN_TEST_TOKEN),
],
});

const swapRouterInterface = SwapRouter.INTERFACE;
const paymentsInterface = PaymentsExtended.INTERFACE;
const exchange = new Exchange(TEST_DEX_CONFIGURATION);

// Sell 100 native tokens for X amount of FUN where the exchange rate is 1 token-in : 10 token-out
// Route is WIMX > USDC > FUN
const { swap } = await exchange.getUnsignedSwapTxFromAmountIn(
TEST_FROM_ADDRESS,
'native',
FUN_TEST_TOKEN.address,
newAmountFromString('100', nativeTokenService.nativeToken).value,
3, // 3 % slippage
);

expectToBeDefined(swap.transaction.data);
expectToBeDefined(swap.transaction.value);
const calldata = swap.transaction.data.toString();

const topLevelParams = swapRouterInterface.decodeFunctionData('multicall(uint256,bytes[])', calldata);

expect(topLevelParams.data.length).toBe(2); // expect that there are two calls in the multicall
const swapTransactionCalldata = topLevelParams.data[0];
const refundETHTransactionCalldata = topLevelParams.data[1];

expectToBeString(swapTransactionCalldata);
expectToBeString(refundETHTransactionCalldata);

const decodedRefundEthTx = paymentsInterface.decodeFunctionData('refundETH', refundETHTransactionCalldata);

expect(topLevelParams.data[1]).toEqual(refundETHFunctionSignature);
expect(decodedRefundEthTx.length).toEqual(0); // expect that the refundETH call has no parameters
});
});

describe('with a native token out', () => {
it('should specify the Router contract as the recipient of the swap function call', async () => {
mockRouterImplementation({
Expand Down
14 changes: 10 additions & 4 deletions packages/internal/dex/sdk/src/lib/transactionUtils/swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,12 @@ function buildSinglePoolSwap(
);
}

const shouldRefundNativeTokens = trade.tradeType === Uniswap.TradeType.EXACT_OUTPUT && isNative(tokenIn);
const shouldRefundNativeTokens = isNative(tokenIn);
if (shouldRefundNativeTokens) {
// Refund ETH if the input token is native and the swap is exact output
// Refund ETH if the input token is native.
// In some cases, the user may have specified an input amount that is greater than what
// the liqudiity of the pool can provide.
// To account for this case, always call `refundETH` to refund any excess native tokens.
calldatas.push(paymentsContract.encodeFunctionData('refundETH'));
}

Expand Down Expand Up @@ -182,9 +185,12 @@ function buildMultiPoolSwap(
);
}

const shouldRefundNativeTokens = trade.tradeType === Uniswap.TradeType.EXACT_OUTPUT && isNative(tokenIn);
const shouldRefundNativeTokens = isNative(tokenIn);
if (shouldRefundNativeTokens) {
// Refund ETH if the input token is native and the swap is exact output
// Refund ETH if the input token is native.
// In some cases, the user may have specified an input amount that is greater than what
// the liqudiity of the pool can provide.
// To account for this case, always call `refundETH` to refund any excess native tokens.
calldatas.push(paymentsContract.encodeFunctionData('refundETH'));
}

Expand Down

0 comments on commit 2212b95

Please sign in to comment.