Direct Trades

(Single Swaps)

Direct trades between base currency and share tokens is most simple way trade share tokens. Especially for aggregators it is recommended to implement these trades.

The following examples shows you how to implement a trade contract, you will implement two functions:

  • buySharesDirect

  • sellSharesDirect

If you looking to integrate the direct trades via ethers please look at the web integration with ethers.

The buySharesDirect function performs an exact output swap, which swaps a minimum possible amount of base currency token for a fixed amount of share token. This function uses the ExactOutputSingleParams struct and the exactOutputSingle function from the ISwapRouter interface.

The sellSharesDirect function an exact input swap, which swaps a fixed amount of share tokens for maximum possible amount of base currency tokens. This function uses the ExactInputSingleParams struct and the exactInputSingle function from the ISwapRouter interface.

For simplification, the example hardcodes the token contract addresses, but as explained further below the contract could be modified to change pools and tokens on a per transaction basis.

When trading from a smart contract, the most important thing to keep in mind is that access to an external price source is required. Without this, trades can be frontrun for considerable loss.

Note: The swap examples are not production ready code, and are implemented in a simplistic manner for the purpose of learning.

Set Up the Contract

Declare the solidity version used to compile the contract.

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity =0.8.21;

Import two relevant contracts from the Uniswap npm package

import '@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol';
import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol';

Create a contract called TradeExamples, and declare an immutable public variable swapRouter of type ISwapRouter. This allows us to call functions in the ISwapRouter interface.

contract TradeExamples {
    ISwapRouter public immutable brokerbotRouter;

Hardcode the base currency and share token contract addresses for the example. In production, you would likely use an input parameter for this and pass the input into a memory variable, allowing the contract to change the brokerbot and tokens it interacts with on a per transaction basis, but for conceptual simplicity, we are hardcoding them here.

    address public constant BASE_TOKEN = 0xB4272071eCAdd69d933AdcD19cA99fe80664fc08; //XCHF
    address public constant SHARE_TOKEN = 0x6f38e0f1a73c96cB3f42598613EA3474F09cB200; // DAKS
    
    constructor(ISwapRouter _brokerbotRouter) {
        brokerbotRouter = _brokerbotRouter;
    }

Buy Shares

The caller must approve the contract to withdraw the base currency tokens from the calling address's account to execute a trade. Remember that because our contract is a contract itself and not an extension of the caller (us); we must also approve the Brokerbot router contract to use the tokens that our contract will be in possession of after they have been withdrawn from the calling address (us).

Then, transfer the amount of XCHF from the calling address into our contract, and use amount as the value passed to the second approve.

/// @notice buySharesDirect trades a minimum possible amount of XCHF for a fixed amount of shares.
/// @dev The calling address must approve this contract to spend its XCHF for this function to succeed. As the amount of input XCHF is variable,
/// the calling address will need to approve for a slightly higher amount, anticipating some variance.
/// @param amountOut The exact amount of shares to receive on this trade
/// @param amountInMaximum The amount of XCHF we are willing to spend to receive the specified amount of shares.
/// @return amountIn The amount of XCHF actually spent in the trade.
function buySharesDirect(uint256 amountOut, uint256 amountInMaximum) external returns (uint256 amountIn) {
        // Transfer the specified amount of base currency tokens (XCHF) to this contract.
        TransferHelper.safeTransferFrom(BASE_TOKEN, msg.sender, address(this), amountInMaximum);

        // Approve the router to spend the specified `amountInMaximum` of base currency tokens (XCHF).
        // In production, you should choose the maximum amount to spend based on oracles or other data sources to achieve a better trade.
        TransferHelper.safeApprove(BASE_TOKEN, address(brokerbotRouter), amountInMaximum);

Input Parameters

To execute the swap function, we need to populate the ExactOutputSingleParams with the necessary swap data. These parameters are found in the smart contract interfaces, which can be browsed here.

A brief overview of the parameters:

  • tokenIn The contract address of base currency token

  • tokenOut The contract address of the share token

  • fee We set this to zero - as the brokerbot router doesn't use it

  • recipient the destination address of the shares token

  • deadline: the unix time after which a swap will fail, to protect against long-pending transactions and wild swings in prices

  • amountOutMinimum: For a real deployment, this value should be calculated using the brokerbot quoter - this helps protect against getting an unusually bad price for a trade due to a front running sandwich or another type of price manipulation

  • sqrtPriceLimitX96: We set this to zero - as the brokerbot router doesn't use it

        ISwapRouter.ExacInputSingleParams memory params =
            ISwapRouter.ExactInputSingleParams({
                tokenIn: BASE_TOKEN,
                tokenOut: SHARE_TOKEN,
                fee: 0,
                recipient: msg.sender,
                deadline: block.timestamp,
                amountIn: amountIn,
                amountOutMinimum: amountInMaximum,
                sqrtPriceLimitX96: 0
            });

Call the Function

       // Executes the trade returning the amountIn needed to spend to receive the desired amountOut.
       amountIn = brokerbotRouter.exactOutputSingle(params);

Pay back overspend

Because this example transfers in the inbound asset in anticipation of the trade - its possible that some of the inbound token will be left over after the trade is executed, which is why we pay it back to the calling address at the end of the trade.

        // For exact output trades, the amountInMaximum may not have all been spent.
        // If the actual amount spent (amountIn) is less than the specified maximum amount, we must refund the msg.sender and approve the swapRouter to spend 0.
        if (amountIn < amountInMaximum) {
            TransferHelper.safeApprove(BASE_TOKEN, address(brokerbotRouter), 0);
            TransferHelper.safeTransfer(BASE_TOKEN, msg.sender, amountInMaximum - amountIn);
        }
    }

Selling Shares

The caller must approve the contract to withdraw the share tokens from the calling address's account to execute a trade. Remember that because our contract is a contract itself and not an extension of the caller (us); we must also approve the brokerbot router contract to use the tokens that our contract will be in possession of after they have been withdrawn from the calling address (us).

Then, transfer the amount of shares from the calling address into our contract, and use amount as the value passed to the second approve.

    /// @notice sellSharesDirect trades a fixed amount of shares for a maximum possible amount of base currency tokens
    /// by calling `exactInputSingle` in the brokerbot router.
    /// @dev The calling address must approve this contract to spend at least `amountIn` worth of its share token for this function to succeed.
    /// @param amountIn The exact amount of share tokens that will be swapped for base currency tokens (XCHF).
    /// @return amountOut The amount of base currecny tokens (XCHF) received.
    function sellSharesDirect(uint256 amountIn) external returns (uint256 amountOut) {
        // msg.sender must approve this contract

        // Transfer the specified amount of share tokens (DAKS) to this contract.
        TransferHelper.safeTransferFrom(SHARE_TOKEN, msg.sender, address(this), amountIn);

        // Approve the router to spend share tokens (DAKS).
        TransferHelper.safeApprove(SHARE_TOKEN), address(brokerbotRouter), amountIn);

Input Parameters

To execute the swap function, we need to populate the ExactInputSingleParams with the necessary swap data. These parameters are found in the smart contract interfaces, which can be browsed here.

A brief overview of the parameters:

  • tokenIn The contract address of the inbound token

  • tokenOut The contract address of the outbound token

  • fee The fee tier of the pool, used to determine the correct pool contract in which to execute the swap

  • recipient the destination address of the outbound token

  • deadline: the unix time after which a swap will fail, to protect against long-pending transactions and wild swings in prices

  • amountOutMinimum: we are setting to zero, but this is a significant risk in production. For a real deployment, this value should be calculated using our SDK or an onchain price oracle - this helps protect against getting an unusually bad price for a trade due to a front running sandwich or another type of price manipulation

  • sqrtPriceLimitX96: We set this to zero - which makes this parameter inactive. In production, this value can be used to set the limit for the price the swap will push the pool to, which can help protect against price impact or for setting up logic in a variety of price-relevant mechanisms.

Call the function

        // Naively set amountOutMinimum to 0. In production, use an oracle or other data source, like the brokerbot quoter, to choose a safer value for amountOutMinimum.
        // We also set the sqrtPriceLimitx96 to be 0 to ensure we swap our exact input amount.
        ISwapRouter.ExactInputSingleParams memory params =
            ISwapRouter.ExactInputSingleParams({
                tokenIn: SHARE_TOKEN,
                tokenOut: BASE_TOKEN,
                fee: 0,
                recipient: msg.sender,
                deadline: block.timestamp,
                amountIn: amountIn,
                amountOutMinimum: 0,
                sqrtPriceLimitX96: 0
            });

        // The call to `exactInputSingle` executes the swap.
        amountOut = swapRouter.exactInputSingle(params);
    }

A Complete Trading Contract

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity =0.8.21;

import '@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol';
import '@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol';

contract TradeExamples {
    ISwapRouter public immutable brokerbotRouter;
    address public constant BASE_TOKEN = 0xB4272071eCAdd69d933AdcD19cA99fe80664fc08; //XCHF
    address public constant SHARE_TOKEN = 0x6f38e0f1a73c96cB3f42598613EA3474F09cB200; // DAKS
    
    constructor(ISwapRouter _brokerbotRouter) {
        brokerbotRouter = _brokerbotRouter;
    }
    /// @notice buySharesDirect trades a minimum possible amount of XCHF for a fixed amount of shares.
    /// @dev The calling address must approve this contract to spend its XCHF for this function to succeed. As the amount of input XCHF is variable,
    /// the calling address will need to approve for a slightly higher amount, anticipating some variance.
    /// @param amountOut The exact amount of shares to receive on this trade
    /// @param amountInMaximum The amount of XCHF we are willing to spend to receive the specified amount of shares.
    /// @return amountIn The amount of XCHF actually spent in the trade.
    function buySharesDirect(uint256 amountOut, uint256 amountInMaximum) external returns (uint256 amountIn) {
        // Transfer the specified amount of base currency tokens (XCHF) to this contract.
        TransferHelper.safeTransferFrom(BASE_TOKEN, msg.sender, address(this), amountInMaximum);

        // Approve the router to spend the specified `amountInMaximum` of base currency tokens (XCHF).
        // In production, you should choose the maximum amount to spend based on oracles or other data sources to achieve a better swap.
        TransferHelper.safeApprove(BASE_TOKEN, address(brokerbotRouter), amountInMaximum);
        ISwapRouter.ExacInputSingleParams memory params =
            ISwapRouter.ExactInputSingleParams({
                tokenIn: BASE_TOKEN,
                tokenOut: SHARE_TOKEN,
                fee: 0,
                recipient: msg.sender,
                deadline: block.timestamp,
                amountIn: amountIn,
                amountOutMinimum: amountInMaximum,
                sqrtPriceLimitX96: 0
            });
        // Executes trade returning the amountIn needed to spend to receive the desired amountOut.
        amountIn = brokerbotRouter.exactOutputSingle(params);
        // For exact output trades, the amountInMaximum may not have all been spent.
        // If the actual amount spent (amountIn) is less than the specified maximum amount, we must refund the msg.sender and approve the swapRouter to spend 0.
        if (amountIn < amountInMaximum) {
            TransferHelper.safeApprove(BASE_TOKEN, address(brokerbotRouter), 0);
            TransferHelper.safeTransfer(BASE_TOKEN, msg.sender, amountInMaximum - amountIn);
        }
    }
    
    /// @notice sellSharesDirect trades a fixed amount of shares for a maximum possible amount of base currency tokens
    /// by calling `exactInputSingle` in the brokerbot router.
    /// @dev The calling address must approve this contract to spend at least `amountIn` worth of its share token for this function to succeed.
    /// @param amountIn The exact amount of share tokens that will be swapped for base currency tokens (XCHF).
    /// @return amountOut The amount of base currecny tokens (XCHF) received.
    function sellSharesDirect(uint256 amountIn) external returns (uint256 amountOut) {
        // msg.sender must approve this contract

        // Transfer the specified amount of share tokens (DAKS) to this contract.
        TransferHelper.safeTransferFrom(SHARE_TOKEN, msg.sender, address(this), amountIn);

        // Approve the router to spend share tokens (DAKS).
        TransferHelper.safeApprove(SHARE_TOKEN), address(brokerbotRouter), amountIn);
        // Naively set amountOutMinimum to 0. In production, use an oracle or other data source, like the brokerbot quoter, to choose a safer value for amountOutMinimum.
        // We also set the sqrtPriceLimitx96 to be 0 to ensure we swap our exact input amount.
        ISwapRouter.ExactInputSingleParams memory params =
            ISwapRouter.ExactInputSingleParams({
                tokenIn: SHARE_TOKEN,
                tokenOut: BASE_TOKEN,
                fee: 0,
                recipient: msg.sender,
                deadline: block.timestamp,
                amountIn: amountIn,
                amountOutMinimum: 0,
                sqrtPriceLimitX96: 0
            });

        // The call to `exactInputSingle` executes the swap.
        amountOut = swapRouter.exactInputSingle(params);
    }

Last updated