If you want maximum arbitrage performance, you need to swap tokens between exchanges in a single transaction. Or maybe you just want to save gas on certain swaps you perform regularly. Or maybe you have your own custom use case for swapping between decentralized exchanges. And of course maybe you are just here for the learning aspect.
Whatever your reason may be, MultiSwap is a great way to combine knowledge into one contract. We have previously looked at a MultiSwap that looks like this:
You can find it here. But this time we are going to upgrade it. Instead of having to deploy a new contract for every MultiSwap, we can build a generic MultiSwap contract based on the same concept as before. Then you deploy the contract only once and use it for any arbitraging you like.

To do that, we will still use the same trade from the previous MultiSwap as example. So how does this new advanced MultiSwap look like?
Advanced MultiSwap

// Historical AdvancedMultiTrade executor withdrawn in the 2026 review.
// Arbitrary caller-selected calls over shared funds and token approvals
// require an authorization and accounting design absent from this sketch.
// This article retains ABI concepts, not a deployable swap executor.

Wow that was easy. How does that work?
Low-level calls can execute ABI-encoded input at a target address, but that flexibility does not establish authorization. The old example forwarded the contract’s entire ETH balance and left approvals and shared token funds exposed to arbitrary caller-selected actions.
gasleft() also does not forward literally every remaining gas unit: EVM call rules include the EIP-150 cap and call costs. Finally, string(returndata) is not a general decoder for ABI-encoded reverts or custom errors.
That's great, but how can you use this???
We did indeed shift the complexity from the contract code itself onto how you interact with it. What you need to know is
- which contract addresses do I want to call
- which function on that address I want to call
- and which parameters I want to pass to that function.
And to make matters worse, all of this needs to be in low level encoded data.
The address itself should be easy to get. In our old MultiSwap example for Ropsten on Banchor that was 0xb3fa5dcf7506d146485856439eb5e401e0796b5d. Now we need to figure out which function to call. On a low-level call this is done from the 4-bytes function selector.
One little trick on how to get that is adding the following function to your contract:
function getSelector(string calldata _func) external pure returns (bytes4) {
return bytes4(keccak256(bytes(_func)));
}And now simply call the function with the function name and parameters:
getSelector("convertByPath(address[],uint256,uint256,address,address,uint256)")Great, now we have the function selector which is 0xb77d239b. Now last we need to encode the data for the function parameters.
Once you fully automate your arbitrage, you will want to do this with your favorite choice of library, like Web3.js, Web3.py or ethers.js. Now for testing, you could use this handy online tool here.
Enter the function parameter types separated by comma as well as the values.
- address[],uint256,uint256,address,address,uint256
- [0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE,0x1aCE5DD13Ba14CA42695A905526f2ec366720b13,0xF35cCfbcE1228014F66809EDaFCDB836BFE388f5],1000,1,0x0000000000000000000000000000000000000000,0x0000000000000000000000000000000000000000,0
See the previous MultiSwap example for the details on this here.

Now we the data required for our first call. Simply copy & paste the encoded string after our function selector and we have the first data field complete:
// Historical Ropsten route payload omitted after withdrawal of the executor.
// The original addresses, amounts and 2024 deadlines are not current inputs.
// Function selector and ABI encoding concepts remain described above.
Constructing the other two calls
The other two calls to SushiSwap and Uniswap follow the exact same principle.
getSelector("swapExactTokensForTokens(uint256,uint256,address[],address,uint256)")will give us the function selector 0x38ed1739. And for the parameter encoding we use:
- uint256,uint256,address[],address,uint256
- 5000000000000000000,2476572156145166857,[0xF35cCfbcE1228014F66809EDaFCDB836BFE388f5,0x9108Ab1bb7D054a3C1Cd62329668536f925397e5],0x2f0cd179a4f3d47eac5a8d22ccb5d76621212616
This historical parameter list was also incomplete: the five-argument function requires a deadline. Its old encoded example used a 2024 timestamp. The payload has been withdrawn with the executor, rather than presented as valid current call data.
Note that as recipient address we used the MultiSwap address itself (0x2f0cd179a4f3d47eac5a8d22ccb5d76621212616). A contract address can be predicted before deployment using the appropriate CREATE or CREATE2 inputs; it must still match the actual deployed contract. And we of course need it to be the contract itself as recipient, so we can use the funds for the last swap.
And that will give us the second call data:
// Historical Ropsten route payload omitted after withdrawal of the executor.
// The original addresses, amounts and 2024 deadlines are not current inputs.
// Function selector and ABI encoding concepts remain described above.
And the same thing for the Uniswap call:
getSelector("exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))")will give us the function selector 0x414bf389. And for the parameter encoding we use:
- address,address,uint24,address,uint256,uint256,uint256,uint160
- 0x9108Ab1bb7D054a3C1Cd62329668536f925397e5,0xaD6D458402F60fD3Bd25163575031ACDce07538D,3000,0x15ae150d7dC03d3B635EE90b85219dBFe071ED35,1728418389,5000,1,0
This original router takes a tuple with a deadline; later router APIs differ. The 2024 timestamp and token addresses here are historical, not fresh trade inputs.
Note that since this is the last call, we used our own address as recipient, not the MultiSwap smart contract address.
Which will give us the third and final call data:
// Historical Ropsten route payload omitted after withdrawal of the executor.
// The original addresses, amounts and 2024 deadlines are not current inputs.
// Function selector and ABI encoding concepts remain described above.
And lastly merge all three calls into one:
// Historical Ropsten route payload omitted after withdrawal of the executor.
// The original addresses, amounts and 2024 deadlines are not current inputs.
// Function selector and ABI encoding concepts remain described above.
What about the token.approve?
Good catch. We are indeed requiring the token approvals from the contract to the DEX contract. How you will integrate those the best will depend on your use case. If you already know which tokens you will exclusively trade, just put them into the constructor:
// Historical unlimited-approval constructor withdrawn with the executor.
// Approval authority must be part of a constrained, tested design.
// The moving OpenZeppelin import was also incompatible with Solidity 0.8.10.
But of course those approvals are also just calls itself. So you could just make them part of the function call arrays.
The original executor has been withdrawn. A safe batching design needs an explicit authorization boundary, per-operation value and token accounting, destination constraints, slippage/deadline rules and complete validation.





Join the conversation
Comments are hosted by Disqus and load only when you choose to enable them.