Code Guide: Automating liquidity locking on UNCX Network using Gelato

UNCX Network
7 min readNov 28, 2022

--

In this tutorial, you will learn how to automate liquidity locking on UNCX Network using Gelato Network.

What is liquidity locking?

Liquidity locking is one of the few tools protocols can use to secure liquidity in a trust-less fashion. When liquidity tokens are locked, they are vested in a locking contract for a specific period, after which the lock owner can withdraw them. Until then, the project will not be able to remove the underlying liquidity from the pool (a.k.a. rugpull), reducing the risk of malicious liquidity removal proportionally to the percentage of the supply locked.

What is a liquidity tax?

A liquidity tax is a popular method to grow the liquidity pool and reduce price impact on large AMM trades, largely popularised by tokens such as SafeMoon.

As tokens get taxed with trades on a decentralized exchange, new LP tokens are created and sent to the protocol’s wallets.

What does liquidity tax mean for the locked liquidity?

  1. One of the main issues caused by the liquidity tax is the dilution of the vested liquidity supply. As new LP token supply increases with volume and is sent to the protocol’s wallet — it gradually renders the lock ineffective overtime at the percentage of the liquidity supply locked diminishes.
  2. Locking newly minted LP tokens is a manual and tedious process that’s prone to human error and relies on the project owner to act in good faith.

To solve these problems, we will create a smart contract wallet that automatically locks newly generated LP tokens on UNCX Network with the help of Gelato Network.

Prerequisites

1. Maintain compatibility with traditional tax token distribution mechanisms.

We want to build a system where token developers don’t have to conform to a new protocol to achieve the same result.

2. Provide maximum flexibility and control over the tokens.

Developers should have complete control over how they design the trigger conditions, for example, locking after a certain period or once enough tokens have been accumulated inside the wallet.

3. Make sure our solution is both secure and reliable.

An unreliable automation layer can lead to missed executions at best and loss of funds at worst. We want our function to execute at a precise time and a predictable gas price range.

Enter Gelato

Gelato is web3’s decentralized backend empowering builders to create augmented smart contracts that are automated, gasless & off-chain aware. Using Gelato’s network of Executors, recurring and conditional processes such as the one described above can be automated in a reliable and trustless fashion. This makes Gelato web3’s most reliable automation provider that is located at the heart of many prominent protocols, including Optimism, MakerDAO or Sushiswap.

How will we build our contract?

UNCX Network exposes multiple functions which can be used to increase the liquidity locked. First is lockLPToken(), which creates a new lock with a new lockID. While creating a new lock could work, there might be better options than generating a new one for every deposit. A better method to use would be incrementLock(). The great thing about incrementLock() is it doesn’t create a new owner but increases the share of the pre-existing owner of the lock. It also takes care of all validity checks, such as ensuring the address of the deposited LP token is correct.

We will then use Gelato to call incrementLock() with custom parameters and settings.

Creating test LP tokens

We will be using the Goerli Testnet for this guide.

If you don’t have a test token, head to https://app.uncx.network/services/minter to mint a new basic ERC20.

The quickest way to create new LP tokens for testing is by providing liquidity on an AMM such as Uniswap.

Double check it’s the version to ensure it’s v2 and a correct testnet chain is selected https://app.uniswap.org/#/pool/v2.

Creating a Lock

Follow the instructions on https://app.uncx.network/amm/univ2-v2-goerli/locker to generate a new liquidity lock. Make sure you have some LP tokens left to add to the pool later in the tutorial.

Once a lock is created, you should be able to find the lock ID at the bottom of the page. Additionally, you could extract the lock ID from the locker contract, transaction data, and events.

The Contract Code

For the following part, you can use Remix or a smart contract library of your choice to deploy the auto liquidity locker. We will be using Hardhat for deploying the contract.

To make our lives easier, we will import the OpenZeppelin Ownable.sol for access control. We will also import the top-level Gelato OpsTaskCreator.sol, which combines all you require to automate a contract in a neat package.

Other contracts that we will be needing are OpsReady.sol and Types.sol

pragma solidity ^0.8.14;

import "@openzeppelin/contracts/access/Ownable.sol";
import "./OpsTaskCreator.sol";

We will then add an interface to interact with UNCX Network’s incrementLock() function.

interface IUniswapV2Locker {
function incrementLock(uint256 _lockID, uint256 _amount) external;
}

After inheriting the OpsTaskCreator and Ownable contracts, we define the following public variables.

contract UncxAutoLocker is OpsTaskCreator, Ownable {

// Gas threshold above which executions should stop
uint256 public gasThreshold = 80 gwei;
// timestamp of the last Gelato execution
uint256 public lastExecuted;
// task ID to track Gelato executions
bytes32 public taskId;

// UNCX Locker Address.
address public lockerAddress;
// Address of the liquidity token to be incremented.
address public lpToken;
// UNCX liquidity lock nonce (lock ID).
uint256 public lockID;
// threshold token amount required for a Gelato execution.
uint256 public amount;
// minimum interval between code executions.
uint256 public interval = 5 minutes;

We will pass all of the necessary parameters to the constructor.

constructor(
address _lockerAddress,
address _lpToken,
uint256 _lockID,
uint256 _amount,
address payable _ops,
address _fundsOwner
) OpsTaskCreator(_ops, _fundsOwner) {
lockerAddress = _lockerAddress;
lpToken = _lpToken;
lockID = _lockID;
amount = _amount;
lastExecuted = block.timestamp;
}

A function that will be automated to call incrementLock(). Notice the onlyDedicatedMsgSender modifier to ensure that only Gelato can call incrementLpLock(). In this function, we will also use contract funds to pay for the calls and update the time of the last execution.

function incrementLpLock(uint256 _amount) external onlyDedicatedMsgSender {
SafeERC20.safeApprove(IERC20(lpToken), lockerAddress, _amount);
IUniswapV2Locker(lockerAddress).incrementLock(lockID, _amount);
(uint256 fee, address feeToken) = _getFeeDetails();
_transfer(fee, feeToken);
lastExecuted = block.timestamp;
}

Here we will be creating and submitting a task to the Gelato Network. We will be using a self-paying Resolver module.

receive() external payable {}

function createTask() external onlyOwner {
require(taskId == bytes32(""), "Already started task");
ModuleData memory moduleData = ModuleData({
modules: new Module[](2),
args: new bytes[](2)
});
moduleData.modules[0] = Module.RESOLVER;
moduleData.modules[1] = Module.PROXY;
moduleData.args[0] = _resolverModuleArg(
address(this),
abi.encodeCall(this.checker, ())
);
moduleData.args[1] = _proxyModuleArg();
bytes32 id = _createTask(
address(this),
abi.encode(this.incrementLpLock.selector),
moduleData,
ETH
);
taskId = id;
emit CounterTaskCreated(id);
}

Refer to Gelato’s developer documentation to read more about different modules:

function cancelTask() external onlyOwner {
_cancelTask(taskId);
taskId = bytes32("");
}

We will unset the taskID and call the inherited _cancelTask() method to cancel the task. To restart the execution, we would call createTask() once again.

function checker()
external
view
returns (bool canExec, bytes memory execPayload)
{
if (tx.gasprice > gasThreshold) {
return (false, bytes("Gas above threshold"));
}

if (IERC20(lpToken).balanceOf(address(this)) < amount) {
return (false, bytes("Amount threshold not met"));
}

canExec = (block.timestamp - lastExecuted) >= interval;
execPayload = abi.encodeWithSelector(this.incrementLpLock.selector, amount);
}

The Gelato Executors will call checker() to check if all the conditions to call incrementLpLock() are met. Failing any of the conditions returns false and can be accommodated by a message viewed on a Gelato task dashboard. https://app.gelato.network/task/[taskID]?chainId=[ID] (Goerli is 5).

View full Gist here.

Deployment Code

An example hardhat deployment script:

import { ethers } from "hardhat";

async function main() {

// Goerli UNCX locker v2 address
let lockerAddress = "0x95cbf2267ddD3448a1a1Ed5dF9DA2761af02202e"

// Replace with the LP token address you want to increment (matching the lock)
let lpToken = "0x"

// Replace with Lock ID retrieved after creating the lock (matching the lock)
let testLockID = "0"

// Amount of tokens required to trigger the Gelato execution
let thresholdAmount = ethers.utils.parseUnits('5', 2).toString()

// Gelato Goerli ops address. For other chains see
// https://docs.gelato.network/developer-services/automate/contract-addresses
let gelatoOps = "0xc1C6805B857Bef1f412519C4A842522431aFed39"
// Replace with an address of the funds over
let fundsOwner = "0x"

// Deploy the contract
const AutoLocker = await ethers.getContractFactory("UncxAutoLocker");
const autoLocker = await AutoLocker.deploy(lockerAddress, lpToken, testLockID, thresholdAmount, gelatoOps, fundsOwner);
await autoLocker.deployed()
console.log(`autolocker deployed to ${autoLocker.address}`);
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

Usage

To test your newly deployed contract, send some locked ETH and LP tokens that you’re trying to increment to the auto locker address. Ensure the amount sent is greater than the amount set by the _amount used when initializing the contract. Then invoke the createTask(). Use the Gelato dashboard to check the status of your task https://app.gelato.network/task/[taskID]?chainId=[ID] (Goerli is 5).

Going Forward

So far, we’ve created a powerful yet simple contract. Some potential features that could be added are ETH and LP balance withdrawals, Lock and Locker contract setters, unlock conditions, and intervals.

This concept can also be applied to UNCX’s other services, such as token vesting and staking.

UNCX Network is planning to roll out a simple auto-locking mechanism that covers most basic cases — so that existing users can take advantage of the technology without writing a single line of code. Writing your own custom auto-locking contracts is still suggested for more advanced instances.

Disclaimer

The examples provided in this guide have not been audited (sorry, bear market). It is your responsibility to make sure this code is suitable for production.

What is UNCX Network?

UNCX Network is a leading protocol in decentralized token launching, vesting, customizable token staking, and liquidity locking.

Over the last few years, UNCX has developed advanced features such as on-chain rebasing support, per-block linear unlocks, and liquidity migration. Using this technology, UNCX Network serves thousands of Defi projects to increase their transparency and boost their user’s trust, becoming the go-to protocol for on-chain security

✍️ This code guide was brought to you by Beary, one of UNCX’s developers.

--

--

UNCX Network
UNCX Network

Written by UNCX Network

Multi-chain decentralized services provider — Built from the ground-up, permanently ignoring market conditions and delivering disruption.