DeFi TWAP: Uniswap and Chainlink

Mar 17, 2024

Background

Uniswap is the premier decentralized exchange with innovations such as concentrated liquidity and aggregated order routing. While it is possible to use the Uniswap User Interface to complete your trades, you might benefit from deploying your own smart contract which will perform trades on your behalf. Using Chainlink Automations, you can add logic to an upkeep function which is triggered by a trust-minimized, decentralized network on a predictable basis. For most people doing simple trades, the user interface is sufficient. But for complicated mandates or larger funds, some custom trade configuration could be beneficial. 

TWAP or time-weighted average price is trade strategy which spreads out a single order in regular intervals over a period of time. Institutional trading products offer the execution of TWAP which achieves a near average execution price in the specified time range. This reduces execution risks like partial fill for limit orders and price or market manipulation. Institutional buyers are more likely to execute a trade over the course of a few hours or even a full day to capture what they believe is a fair price within the allotted time interval. 

Present Value

The newly launched "Duncun" upgrade to the Ethereum network will reduce the cost of performing transactions on Layer-2 networks. This reduction in fees taken by the blockchain for performing operations will enable new kinds of frequency-based financial operations, such as diversified trade types.

In fact, many operations on Layer-2 networks will see costs reduced to nearly-zero; adding all but the highest-frequency trading techniques to the toolkit of decentralized funds. See an estimate from CoinBureau on X: https://twitter.com/coinbureau/status/1768660393684038108

Using the learnings of institutional traders in traditional finance, you can benefit from TWAP in decentralized trading by using verifiable, off-chain execution network like those offered by Chainlink Automations. This guide will show you how to implement this institutional trading strategy for your crypto fund and sharpen your edge. 

Be cautious when creating smart contracts. You could lose any funds deposited to a security or requirement bug, so be cautious. This guide is for experienced DeFi traders and Solidity developers alike who want to execute sophisticated trading strategies using only decentralized infrastructure.

Technical Walkthrough

Let's start with a simple trade smart contract we'll call TWAP. This contract will be responsible for accepting deposit of the first token in our swap pair, performing the incremental fixed size trades, and ultimately allow withdrawal of the second token in our trading pair.

Let's first implement the depositToken and withdrawalToken functions

// TWAP.sol
function depositToken(uint256 amount) external onlyOwner {
    require(amount > 0, "Deposit amount must be greater than zero.");
    IERC20(tokenA).transferFrom(msg.sender, address(this), amount);
}

function withdrawToken(uint256 amount) external {
    require(amount > 0, "Withdrawal amount must be greater than zero.");
    bool success = IERC20(tokenB).transfer(owner, amount);
    require(success, "Transfer failed.");
}

Then we will make a simple swap using the Uniswap ISwapRouter interface. In this implementation we use the ExactInputSingleParams function on the router, which allows us to specify an exact input value of one token for a variable output of another token, including a lower-bound amountOutMinimum and an explicit limit-price. A deadline setting of block.timestamp indicates that the swap should be done with atomicity.

// TWAP.sol
function swapTokens() external {
    IERC20(tokenA).approve(address(swapRouter), amountA);

    // Perform the swap.
    ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
        tokenIn: tokenA,
        tokenOut: tokenB,
        fee: 3000,
        recipient: address(this),
        deadline: block.timestamp,
        amountIn: min(amountA, currentBalance),
        amountOutMinimum: 1, // you can calculate or parameterize a limit value
        sqrtPriceLimitX96: 0
    });
    swapRouter.exactInputSingle(params);

    emit SwapPerformed(IERC20(tokenA).balanceOf(address(this)), 
                       IERC20(tokenB).balanceOf(address(this)));
}

It's helpful to understand the Ethereum network (and by extension the Layer-2 scaling solutions which settle ultimately on the network) as a stateful clock. The network does not have the capability to execute transactions on your behalf. There are solutions such as the Ethereum Alarm Clock and now Chainlink Automations to perform actions on our behalf.

We will define a TimedSwapUpkeep contract which solve our caller-source issue using decentralized infrastructure provided by Chainlink. To properly interact with Chainlink automation we must implement two functions, a checkUpkeep and performUpkeep. You can find out more about the necessary interface here: https://docs.chain.link/chainlink-automation/guides/compatible-contracts

// TimedSwapUpkeep.sol
function checkUpkeep(bytes calldata) 
  external view override returns (bool upkeepNeeded, bytes memory) {
    upkeepNeeded = (block.timestamp - lastTimeStamp) > interval;
    upkeepNeeded = upkeepNeeded || 
      (twapContract.balanceOf(twapContractAddress, tokenAContract) > 1);
    return (upkeepNeeded, bytes(""));
}

function performUpkeep(bytes calldata) external override {
    if ((block.timestamp - lastTimeStamp) > interval ) {
        lastTimeStamp = block.timestamp;
        twapContract.swapTokens();
        emit UpkeepPerformed(block.timestamp, interval);
    }
}

The deployment of the contract to the Layer-2 scaling solution of your choice is simple, see this example for Arbitrum:

// deploy.ts (hardhat)
async function main() {
    const [deployer] = await ethers.getSigners();

    // TWAP
    const ARB_SEPOLIA_WBTC = '0x7f908D0faC9B8D590178bA073a053493A1D0A5d4';
    const ARB_SEPOLIA_USDC = '0xf3C3351D6Bd0098EEb33ca8f830FAf2a141Ea2E1';
    const ARB_UNISWAP_ROUTER = '0x101F443B4d1b059569D643917553c771E1b9663E';

    const Contract = await ethers.getContractFactory("TWAP");
    const contract = await Contract.deploy(ARB_SEPOLIA_USDC, 
                                           ARB_SEPOLIA_WBTC, 
                                           ARB_UNISWAP_ROUTER);

    await contract.deployed();
    console.log("TWAP Contract deployed to:", contract.address);


    // TimedSwapUpkeep
    const TimedSwapUpkeep = await ethers.getContractFactory("TimedSwapUpkeep");
    const timedSwapUpkeep = await TimedSwapUpkeep.deploy(600, contract.address); // 600 seconds
    
    await timedSwapUpkeep.deployed();
    console.log("TimedSwapUpkeep Contract deployed to:", timedSwapUpkeep.address);

}

Once you have deployed your contracts to an EVM-compatible network, you'll navigate to the Chainlink Automation registration page: https://functions.chain.link/mainnet and register a new "time-based upkeep".

This upkeep will contact your checkUpkeep function on the agreed upon cron schedule string. Following the patterns of institutional traders, we may opt for a 10-minute execution pattern which is expressed in cron as

Although you may choose any pattern which meets your trading needs.

Next, fund your upkeep with a few LINK tokens which will pay a for use of the decentralized keeper network and gas fees of executing the functions on your smart contracts. Recall that fees will see a large reduction with the newly deployed Dencun upgrade, so beware of the savings you'll encounter and fund upkeeps accordingly.

At the end of the deployment and registration, you'll have a fully autonomous TWAP trade executor which will begin performing swaps at the next upkeep the TWAP contract is funded with the first token in your trading pair. See the repo for full code examples with mock tokens, which work better on testnet https://github.com/irasigman/defi-twap

Afterwards

If you liked what you read here, follow me on X at @0x1ra or @0xIra on Farcaster.