🔨Plugin Development

Introduction

Algebra introduces plugins, custom smart contracts connected to a pool, allowing developers to inject custom logic into pool lifecycle events (e.g., swaps, liquidity provision). Plugins enable advanced features like dynamic fees, limit orders, TWAP oracles, and more.

In this guide we are going to walk through the process of creating and testing Algebra Plugin. As an example, let's take a plugin that determines the commission in a pool

Which questions we will answer:

  • How to start writing a plugin

  • How the pool interacts with a plugin

  • What are the flags

  • How to earn fees with a plugin

Setup

Algebra provides a repository template with required dependencies, configs and examples of source code and tests.

Clone this repository to setup a project:

git clone https://github.com/cryptoalgebra/algebra-plugin-template
cd algebra-plugin-template
// requires node with npm
npm install

Basic things

Let's create our plugin DynamicFeePlugin.sol by starting with this code:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import '@cryptoalgebra/integral-core/contracts/libraries/Plugins.sol';
import "@cryptoalgebra/integral-base-plugin/contracts/base/BasePlugin.sol";

contract DynamicFeePlugin is BasePlugin {
    using Plugins for uint8;

    uint8 public constant override defaultPluginConfig =
    uint8(Plugins.BEFORE_SWAP_FLAG | Plugins.BEFORE_POSITION_MODIFY_FLAG | Plugins.DYNAMIC_FEE);

    constructor(
        address _pool,
        address _pluginFactory
    ) BasePlugin(_pool, _pluginFactory) {}
}

Above code snippet does the following:

  • Imports Plugins library and BasePlugin abstract contract

  • Sets defaultPluginConfig with before_swap, before_position_modify, dynamic_fee flags

  • Initialises BasePlugin's constructor by providing pool's and plugin factory's addresses

Setting such flags means that we would like a pool to call beforeSwap, beforeModifyPosition hooks and that our plugin is going to manipulate a pool's fees. We chose these particular hooks because we would like to demonstrate how to alter fees on swap and burn interactions.

Pool's and plugin factory's addresses are going to be passed when the plugin is deployed by plugin factory.

Now we can proceed to adding HOOOKS!!!

Firstly, we should add beforeInitialize hook handler. This one is going to be called when the pool is initialized. In that handler we want to set plugin config (flags) in the pool. Primarily, this is done to limit hooks which are called on the plugin to save gas.

/// @dev Will be called regardless of flags
function beforeInitialize(address, uint160) external override onlyPool returns (bytes4) {
    // Here we initially set the flags
    _updatePluginConfigInPool(defaultPluginConfig);
    return IAlgebraPlugin.beforeInitialize.selector;
}

Then, the same way we are doing with all other hooks that we would not like to process:

/// @dev Considering defaultPluginConfig, is unused
function afterInitialize(address, uint160, int24 tick) external override onlyPool returns (bytes4) {
    // Add this line to reset config since the hook is unused
    _updatePluginConfigInPool(defaultPluginConfig);
    return IAlgebraPlugin.afterInitialize.selector;
}

/// @dev Considering defaultPluginConfig, is unused
function afterModifyPosition(address, address, int24, int24, int128, uint256, uint256, bytes calldata) external override onlyPool returns (bytes4) {
    // Add this line to reset config since the hook is unused
    _updatePluginConfigInPool(defaultPluginConfig);
    return IAlgebraPlugin.afterModifyPosition.selector;
}

/// @dev Considering defaultPluginConfig, is unused
function afterSwap(address, address, bool zeroToOne, int256, uint160, int256, int256, bytes calldata) external override onlyPool returns (bytes4) {
    // Add this line to reset config since the hook is unused
    _updatePluginConfigInPool(defaultPluginConfig);
    return IAlgebraPlugin.afterSwap.selector;
}

/// @dev Considering defaultPluginConfig, is unused
function beforeFlash(address, address, uint256, uint256, bytes calldata) external override onlyPool returns (bytes4) {
    // Add this line to reset config since the hook is unused
    _updatePluginConfigInPool(defaultPluginConfig);
    return IAlgebraPlugin.beforeFlash.selector;
}

/// @dev Considering defaultPluginConfig, is unused
function afterFlash(address, address, uint256, uint256, uint256, uint256, bytes calldata) external override onlyPool returns (bytes4) {
    // Add this line to reset config since the hook is unused
    _updatePluginConfigInPool(defaultPluginConfig);
    return IAlgebraPlugin.afterFlash.selector;
}

Implied, that these hook would not be called at all. But if for some reason the config is set wrong in a pool. We should set config in a pool to our defaultPluginConfig, disabling unwanted hooks.

Custom Logic

Authorization

Finally, we have implemented everything needed to proceed to our custom logic. Although, if we try to compile the code, we will get an error:

TypeError: Contract "DynamicFeePlugin" should be marked as abstract.                                                                                                        11s
 --> contracts/DynamicFeePlugin.sol:7:1:
  |
7 | contract DynamicFeePlugin is BasePlugin {
  | ^ (Relevant source part starts here and spans across multiple lines).
Note: Missing implementation: 
  --> @cryptoalgebra/integral-base-plugin/contracts/base/BasePlugin.sol:40:3:
   |
40 |   function _authorize() internal virtual view;
   |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

This is because we have to implement authorization mechanism in our plugin. Authorization is necessary since it determines who is able to collect plugin fees inside BasePlugin. Additionally we are going to need authorization for persmissioned actions with our plugin. Algebra suggests such implementation:

function _authorize() internal override {
    require(msg.sender == pluginFactory, 'Unauthorized');
}

Such implementation will not allow anyone except plugin factory to call authorised functions. Thus as a plugin developer you should add needed functionality to a plugin factory to be able to call these functions later. Or you can implement authorization the other way, for instance like that:

function _authorize() internal view override {
    require(msg.sender == pluginFactory.owner(), 'Unauthorized');
}

The above one requires a caller to be the owner of plugin factory (it should adhere to Ownable ). After all, it's up to you how to implement authorization mechanism in plugin, but it has to be secure.

Custom variables and setters

Proceeding to a custom logic, as a reminder, we would like to develop a plugin which manipulates a pool's fee. Let's introduce some variables inside our plugin:

contract DynamicFeePlugin is BasePlugin {
    // ... //
    
    uint24 public overrideFee;
    uint24 public pluginFee;

    constructor(
        address _pool,
        address _pluginFactory,
        uint24 _overrideFee,
        uint24 _pluginFee
    ) BasePlugin(_pool, _pluginFactory) {
        overrideFee = _overrideFee;
        pluginFee = _pluginFee;
    }
    
    // ... //
}

Above, we added overrideFee and pluginFee which are going to be used later. OverrideFee is going to define LP's fee. PluginFee is going to define fee that belongs to a plugin. A total fee of a swap would be a sum of overrideFee and pluginFee. You should also consider that plugin will probably not receive the exact fee amount defined by pluginFee. Because in reality communityFee (protocol fee) is going to be deducted from calculated plugin fee amount. As well as from LP's fee.

Since we now have some variables, we should add setters to them in case we would want to change their values:

function setParams(uint24 _overrideFee, uint24 _pluginFee) external {
    _authorize();
    overrideFee = _overrideFee;
    pluginFee = _pluginFee;
}

The code above is self-explainatory with a little addition in the form of our previously implemented _authorize() function.

Hooks

Now we are ready to define a logic of handling beforeSwap and beforeModifyPosition. Let's start with beforeSwap:

function beforeSwap(
    address,
    address,
    bool zeroToOne,
    int256,
    uint160,
    bool,
    bytes calldata
) external override onlyPool returns (bytes4, uint24, uint24) {
    if (zeroToOne) {
        // If zeroToOne then we return override fee 2 times lower
        return (IAlgebraPlugin.beforeSwap.selector, overrideFee / 2, pluginFee);
    } else {
        return (IAlgebraPlugin.beforeSwap.selector, overrideFee, pluginFee);
    }
}

beforeSwap hook has to return: it's selector, override fee, plugin fee. For illustration purposes, we implemented the hook in a way that:

  • if someone swaps in a zeroToOne direction, plugin sets LP fee to overrideFee / 2

  • if someone swaps in a oneToZero direction, plugin sets LP fee to overrideFee

If you would not like to manipulate LP fee at all in your plugin, it just has to return 0 as overrideFee.

Further, since we want to charge plugin fee also on burn operations, we have to implement beforeModifyPosition hook:

function beforeModifyPosition(address, address, int24, int24, int128, bytes calldata) external override onlyPool returns (bytes4, uint24) {
    return (IAlgebraPlugin.beforeModifyPosition.selector, pluginFee);
}

Here as well the hook returns it's selector and plugin fee. beforeModifyPosition is called on both mint and burn operations, but plugin fee is charged only on burn.

Plugin Fees

BasePlugin provides basic functionality for plugin fees, that could be overridden optionally.

Firstly, it is possible to override handlePluginFee:

/// @inheritdoc IAlgebraPlugin
function handlePluginFee(uint256, uint256) external view override onlyPool returns (bytes4) {
  return IAlgebraPlugin.handlePluginFee.selector;
}

This function in permitted only to a pool and is called every time when plugin fees are transferred to a plugin. Implementation of this handler inside BasePlugin does not do anything with plugin fees, so they are just accrued on its balance.

Secondly, there is an option to override collectPluginFee:

/// @inheritdoc IBasePlugin
function collectPluginFee(address token, uint256 amount, address recipient) external override {
  _authorize();
  SafeTransfer.safeTransfer(token, recipient, amount);
}

Considering how handlePluginFee works by default, BasePlugin provides a functionality to collect plugin fee which is essentially a transfer of any token from a plugin to a provided recipient.

Plugin Factory

Plugin Factories play essential role in Algebra Integral architecture. Since Algebra sticks to a approach with dedicated plugins for each pool, plugin factories deploy plugins for new or existing pools. Depending on a chosen authorisation mechanism in a plugin, plugin factory might be also a managment point of its plugins.

In order to be able to deploy or DynamicFeePlugin, lets create DynamicFeePluginFactory starting with this code:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import './DynamicFeePlugin.sol';

import '@cryptoalgebra/integral-base-plugin/contracts/base/BasePluginFactory.sol';

contract DynamicFeePluginFactory is BasePluginFactory {
    uint24 public defaultOverrideFee = 10000;
    uint24 public defaultPluginFee = 10000;

    constructor(address _entryPoint) BasePluginFactory(_entryPoint) {}

    function _createPlugin(address pool) internal override returns (address) {
        DynamicFeePlugin plugin = new DynamicFeePlugin(
            pool,
            address(this),
            defaultOverrideFee,
            defaultPluginFee
        );
        return address(plugin);
    }
}

Breaking down the code above:

  • Import BasePluginFactory which simplifies the development process

  • Define defaultOverrideFee and defaultPluginFee which represent values with which new plugins are going to be deployed

  • Initialize entryPoint variable with its value in the constructor

  • Initialises BasePluginFactory's constructor by providing AlgebraCustomPoolEntryPoint address

As we mentioned earlier, plugin factory is not only a main point of deploying custom pools with plugins but also a managment point. Thats why we also have to implement some authorization to it, for example using Ownable by OpenZeppelin:

// ... //
import '@openzeppelin/contracts/access/Ownable.sol';

contract DynamicFeePluginFactory is BasePluginFactory, Ownable {
    // ... //
}

Now our plugin factory has authorization mechanism and we can proceed to implementing differrent managment functions. Let's start with the ones required by BasePluginFactory:

function setTickSpacing(address pool, int24 newTickSpacing) onlyOwner external {
    IAlgebraCustomPoolEntryPoint(entryPoint).setTickSpacing(pool, newTickSpacing);
}

function setPlugin(address pool, address newPluginAddress) onlyOwner external {
    IAlgebraCustomPoolEntryPoint(entryPoint).setPlugin(pool, newPluginAddress);
}

function setPluginConfig(address pool, uint8 newConfig) onlyOwner external {
    IAlgebraCustomPoolEntryPoint(entryPoint).setPluginConfig(pool, newConfig);
}

function setFee(address pool, uint16 newFee) onlyOwner external {
    IAlgebraCustomPoolEntryPoint(entryPoint).setFee(pool, newFee);
}

We implemented:

  • setTickSpacing - sets a tick spacing in a provided pool

  • setPlugin - sets a plugin in a provided pool

  • setPluginConfig - sets plugin config in a provided pool

  • setFee - sets fee in a provided pool. DYNAMIC_FEE flag must be False in a particular pool in order to be able to call setFee on this pool.

pool parameter has to be a pool which is authorized to that factory, thus it can be only the pool which is deployed by the that plugin factory.

As you can see, all the functions above are protected by onlyOwner modifier.

Now, we can add some custom logic to our plugin factory. In our case we might want change the default parameters (overrideFee and pluginFee) used to deploy new plugins. Therefore, let's add a setter:

function setDefaultParams(uint24 _defaultOverrideFee, uint24 _defaultPluginFee) onlyOwner external {
    defaultOverrideFee = _defaultOverrideFee;
    defaultPluginFee = _defaultPluginFee;
}

For simplicity let it be the only one function which sets both parameters. This one is protected by onlyOwner as well.

Last updated