SoroswapRouter Comparison

The Router Contract: Comparison between Soroswap and UniswapV2

Until now, we have developed the Factory and the Pair contract. The factory knows how many pairs it has created and can give us their contract addresses. Also, if we want to trade between token A and token B, and if that pair exists, the Factory will also give us the corresponding pair contract address.

But what happens if there are two pairs, A-B and B-C, and a user wants to make a trade between A and C? We could first trade A to B and then B to C! This is why we have the Router Contract.

The Router Contract allows swapping of tokens when a direct pair does not exist. It also handles liquidity provision and manages deposit and withdrawal functions for liquidity providers within the Soroswap ecosystem.

The native token: ETH, WETH, and XLM.

In the UniswapV2Router02, there is a very important distinction between any ERC20 tokens and the native token ETH (and its wrapped version WETH). In particular, in the contract we find:

address public immutable override WETH;

With UniswapV2Router02, if you are using ETH, you'll need to use special functions:

  • addLiquidityETH instead of addLiquidity

  • removeLiquidityETH instead of removeLiquidity

  • removeLiquidityETHWithPermit instead of removeLiquidityWithPermit

  • Instead of just having swapExactTokensForTokens and swapTokensForExactTokens; Uniswap Router needs to add 4 extra functions: swapExactETHForTokens, swapTokensForExactETH, swapExactTokensForETH, swapETHForExactTokens.

  • Instead of just having swapExactTokensForTokensSupportingFeeOnTransferTokens, Uniswap Router needs to add 2 extra functions: swapExactETHForTokensSupportingFeeOnTransferTokens and swapExactTokensForETHSupportingFeeOnTransferTokens

This is because it's very common that in Blockchains, the native token (in this case ETH in Ethereum) is treated differently, so the smart contract needs to trigger different functions when transferring the native token.

What about in Soroban?

Well, in Soroban, we won't need to have all these extra functions! This is because the native XML token has its own token contract address and can be treated as any other token complying with the token interface.

You can find more information about this in the Soroban Token Playground Chapter 8, 9, 10, and 11.

Conclusion: Only the following functions will be written:

function addLiquidity
function removeLiquidityWithPermit
function swapExactTokensForTokens
function swapTokensForExactTokens
function swapExactTokensForTokensSupportingFeeOnTransferTokens

The following functions won't be written:

function addLiquidityETH
function removeLiquidityETH
function removeLiquidityETHWithPermit
function swapExactETHForTokens
function swapTokensForExactETH
function swapExactTokensForETH
function swapETHForExactTokens
function swapExactETHForTokensSupportingFeeOnTransferTokens
function swapExactTokensForETHSupportingFeeOnTransferTokens

Also, because we will treat XML as any other token, we won't need to define its address. We won't need to write something like:

address public immutable override WETH;

Finally, there is also no need for this function:

receive() external payable {
        assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract
    }

as XML can only be sent using the native contract.

SafeMath

In Solidity, the SafeMath library is used to validate arithmetic operations and prevent integer overflow/underflow. Should such a situation arise, the library throws an exception, which effectively reverts the transaction.

In Rust, we can achieve similar functionality by activating the overflow check flag during the compilation process with the following code:

[profile.release]
overflow-checks = true

Additionally, we use an overflow-safe implementation of functions checked_add, checked_mul, checked_div, and checked_sub. You can explore these functions and test their functionality in this repository: Overflow Soroban Repository

Also, we have overflow-safe functions checked_add, checked_mul, checked_div and checked_sub

You can check and test these techniques in the following repository: Overflow Soroban Repository

In conclusion, Soroswap prevents overflows by leveraging these techniques.

Deadlines

In Ethereum we can send a transaction with low gas price, and that transaction could be accepted 5, 10 or more minutes later. In order to avoid unwanted transactions after a period of time, Uniswap introduces the deadline parameter and the ensure(deadline) modifier:

modifier ensure(uint deadline) {
        require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
        _;
    }

In Soroban, we can think that having a transaction waiting for minutes is something that can never happen. But because we don't want to say never, Soroswap will include this "deadline modifier."

Modifiers do not currently exist in the soroban-sdk. So we'll need to build a special function using the env.ledger().timestamp() object.

Permit

In UniswapV2Router02 we use the permit method of the UniswapV2ERC20.sol contracts that is defined here:

 function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
        require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
        bytes32 digest = keccak256(
            abi.encodePacked(
                '\x19\x01',
                DOMAIN_SEPARATOR,
                keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
            ) 
        );
        address recoveredAddress = ecrecover(digest, v, r, s);
        require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
        _approve(owner, spender, value);
    }

This function implements EIP-2612. This avoids the user to use 2 transactions approve and `transfer, which allows users to modify the allowance mapping using a signed message. this function allows a token holder to grant permission for a specific address (spender) to spend their tokens up to a certain amount (value) with a specified expiration time (deadline). The signature ensures the authenticity of the permit, and the function enforces that the permit hasn't expired and that it was signed by the legitimate token owner.

In Soroban we don't need to implement something like this, as we use the require.auth() method when sending tokens from the user into the smart contract.

Conclusion: We don't need to impement the removeLiquidityWithPermit, neither the removeLiquidityETHWithPermit functions (neither removeLiquidityETHWithPermitSupportingFeeOnTransferTokens)

Fees on Transfer

In UniswapV2, there is a special function that allows the user to swap tokens that charge a fee when doing a transfer. This tokens are popular in the deflactionist community, and indeed it made Uniswap upgrade their original UniswapV2Router (latter called UniswapV2Router01) contract to a UniswapV2Router02 version!

You can read all the discussion of the Uniswap design in this issue

In the code we see many functions that have the word SupportingFeeOnTransferTokens:

  • _swapSupportingFeeOnTransferTokens

  • swapExactTokensForTokensSupportingFeeOnTransferTokens

  • swapExactETHForTokensSupportingFeeOnTransferTokens

  • swapExactTokensForETHSupportingFeeOnTransferTokens

  • removeLiquidityETHSupportingFeeOnTransferTokens

But in fact there is only two functions that do all the work: _swapSupportingFeeOnTransferTokens and removeLiquidityETHSupportingFeeOnTransferTokens

These functions work equal to _swap and removeLiquidityETH but succeeds for tokens that take a fee on transfer.

Because in Soroswap we want to support all types of tokens, we will implement these logics in the SoroswapRouter contract!

library UniswapV2Library

UniswapV2Router uses the UniswapV2Library. This is something that Soroswap will also develop

What's next?

Write rust!

Last updated