🎁Farm Manager

The Farm Manager is a monolithic contract that handles all the farms-related logic for the pools.

How it works

The Farm Manager has two main concepts; a Farm, containing a reward to be distributed and a Position, defined as a user's liquidity in a pool locked in the contract.

Users of the Liquidity Hub, when providing liquidity, can opt to lock their LP shares which will in turn send them to the Farm Manager until they are unlocked.

Farms

Creating farms is permissionless, and farms can be perpetual. This means they can be expanded forever. Anyone can create a farm by calling the ManageFarm message with the FarmAction::Fill action and paying the farm creation fee, which is sent to the Fee Collector.

Users can decide to provide an identifier, which they can later use to top up or close the farm. If no identifier is provided, the contract will generate one.

The farm identifiers have a prefix, if the farm is created without an identifier it will get the prefix f-, followed by a number. On the other hand, if the farm is created with a custom identifier, the prefix will be m-, followed by the custom identifier provided.

Topping up a Farm

To top up a farm, the owner of the farm must call ManageFarm with the FarmAction::Fill action. The user must provide the identifier of the farm it intends to top up, including the prefix assigned by the contract. The farm can only be topped up with the same token as the original reward, and the amount must be a multiple of the original reward's amount.

A farm is considered expired after the time defined in the contract configuration as farm_expiration_time has passed once the farm has ended. Once a farm is expired it cannot be expanded, and expired farms are closed automatically when creating new farms.

Closing a Farm

To close a farm, the owner of the farm or the owner of the contract must call ManageFarm with the FarmAction::Close action with the identifier of the farm to be closed, including the prefix assigned by the contract. The farm will be closed, and the remaining tokens will be sent to the owner of the farm.

Reward Distribution

Farm rewards are distributed every epoch, and the user can claim rewards for as long as the farm is live. Based on the parameters used when creating the farm, the rewards will get an emission rate per epoch. The emission rate is the amount of tokens distributed per epoch.

The user rewards are calculated as follows:

user rewards=lp=aNdenomsf=bNfarmse=startcurrentulpwetlpwere\text{user rewards} = \sum_{\text{lp}=a}^{N_\text{denoms}} \sum_{f=b}^{N_\text{farms}} \sum_{e=\text{start}}^{\text{current}} \frac{u\text{lp}\text{w}_e}{t\text{lp}\text{w}_e} \cdot r_e

where lp are the unique LP denoms the user has locked in the contract, f are the active farms, e the epochs, ranging from the start epoch, which is either the epoch at which the farm starts, or the subsequent epoch after the user's last claimed epoch, until the current epoch which is the one at the moment the claim or query is made. ulpw is the user's LP weight, tlpw is the total LP weight, and r is the reward allocation for a given epoch.

The maximum number of concurrent farms for a given LP denom is defined when the contract is instantiated, and it is stored in the config as max_concurrent_farms.

Note: If a user has pending rewards the moment a farm is closed, the user loses the possibility to claim those tokens as all unclaimed tokens are reimbursed to the farm owner.

Positions

Positions can be created, expanded (topped up), or withdrawn. This is done via the ManagePosition message, followed by the desired action, i.e. PositionAction::Create, PositionAction::Expand, PositionAction::Close or PositionAction::Withdraw.

When a user creates a position, it must provide an unlocking duration and an identifier (optional). The longer the unlocking duration, the higher the weight of the LP position, which equates to higher rewards. Choosing an unlocking duration of one day gives the user a multiplier of 1x, while the maximum period of 365 days gives the user a multiplier of 16x.

Like the farm identifiers, the position identifiers also get a prefix assigned by the contract. If the position is created without a custom identifier, it will get the prefix p-, followed by a number. On the other hand, if the position is created with a custom identifier, the prefix will be u-, followed by the custom identifier provided.

The unlocking duration is the time it takes in seconds to unlock the position, which is necessary to withdraw the LP tokens from the contract.

There's a limit of 100 open positions, as well as 100 closed positions per user.

Note: The unlocking duration is defined as how long the user needs to wait after hitting the Close action to be able to withdraw the LP tokens from the contract. Not to be confused with how long the LP tokens are locked in the farm. The LP tokens are locked in the farm for as long as the user has a position in the farm manager.

Topping up a Position

To expand a position, the user must call ManagePosition with the PositionAction::Expand action using the position identifier of the position to expand. Expanding a position only involves adding more LP tokens to the position. The unlocking period of the original position is kept.

Only open positions can be expanded.

Closing a Position

When a user creates a position, the LP tokens are locked in the contract. The user can't withdraw them until the unlocking duration is complete. The clock starts ticking the moment the user closes the position.

Closing a position is done by calling ManagePosition with the PositionAction::Close action. The user must provide the identifier of the position to be closed. Once this action is triggered, the Position.open state is set to false, and expiring_at is set to the block height after which the position will be able to be withdrawn.

It is possible to close a position partially by providing the amount of LP tokens to remove from the position.

Withdrawing a Position

Once the unlocking duration is complete after closing a position, the user can withdraw the LP tokens from the contract by calling the ManagePosition with the PositionAction::Withdraw action. Alternatively, if the user doesn't want to wait for the unlocking duration to complete, it is possible to do an emergency withdrawal by passing true on the emergency_unlock parameter. This will unlock and withdraw the position immediately, and the user will pay a penalty fee in case the unlocking period has not passed. Half of the penalty is distributed among the farm owners of the given LP, while the other half goes to the Fee Collector.

Once the user closes and withdraws the position, they receive their LP tokens back.

Note: The emergency unlock feature can be used on both open and closed positions. However, if this feature is used, the user renounces all unclaimed rewards for the given position.

Claiming Farm Rewards

Users can claim farm rewards from active farms for their LP tokens, only if they have a position in the farm manager. Users can only claim rewards for future epochs, i.e. after the epoch in which the position was created.

Farm rewards are distributed based on the user's share of the total LP tokens in the contract. So if there's a total of 100 LP tokens in the contract, and a user has 10 LP tokens, the user will receive 10% of the rewards for that epoch, for that given farm.

To claim rewards, the user must call the Claim message. Once that's done, the contract will save the epoch in which the claim was made in LAST_CLAIMED_EPOCH, and will sync the user's LP weight history saved in LP_WEIGHT_HISTORY. This helps computing the rewards for the user.


Instantiate

Instantiates an instance of the farm manager contract

{
  "owner": "mantra1...",
  "epoch_manager_addr": "mantra1...",
  "fee_collector_addr": "mantra1...",
  "pool_manager_addr": "mantra1...",
  "create_farm_fee": {
    "denom": "uom",
    "amount": "1000000000"
  },
  "max_concurrent_farms": 7,
  "max_farm_epoch_buffer": 14,
  "min_unlocking_duration": 86400,
  "max_unlocking_duration": 31536000,
  "farm_expiration_time": 2629746,
  "emergency_unlock_penalty": "0.01"
}
KeyTypeDescription

owner

String

The owner of the contract.

epoch_manager_addr

String

The epoch manager address, where the epochs are managed.

fee_collector_addr

String

The address of the fee collector, where protocol fees go.

pool_manager_addr

String

The address of the pool manager, used to verify the LP tokens locked in the contract.

create_farm_fee

Coin

The fee that must be paid to create a farm.

max_concurrent_farms

u32

The maximum amount of farms that can exist for a single LP token at a time.

max_farm_epoch_buffer

u32

New farms are allowed to start up to current_epoch + start_epoch_buffer into the future.

min_unlocking_duration

u64

The minimum amount of time that a user can lock their tokens for. In seconds.

max_unlocking_duration

u64

The maximum amount of time that a user can lock their tokens for. In seconds.

farm_expiration_time

u64

The amount of time after which a farm is considered to be expired after it ended. In seconds. Minimum a month. Once a farm is expired it cannot be expanded, and expired farms can be closed.

emergency_unlock_penalty

Decimal

The penalty for unlocking a position before the unlocking duration finishes. In percentage.

ExecuteMsg

ManageFarm

Manages a farm based on the action, which can be:

  • Fill: Fills a farm. If the farm doesn't exist, it creates a new one. If it exists already, it expands it given the sender created the original farm and the params are correct.

  • Close: Closes a farm with the given identifier. If the farm has expired, anyone can close it. Otherwise, only the farm creator or the owner of the contract can close a farm.

Once created, farms are stored in the FARMS map.

{
  "manage_farm": {
    "action": {
      "fill": {
        "params": {
          "lp_denom": "factory/mantra1.../LP",
          "start_epoch": 10,
          "preliminary_end_epoch": 24,
          "curve": "linear",
          "farm_asset": {
            "denom": "uom",
            "amount": "1000000000"
          },
          "farm_identifier": "farm_identifier"
        }
      }
    }
  }
}
KeyTypeDescription

lp_denom

String

The LP asset denom to create the farm for.

start_epoch

Option<u64>

The epoch at which the farm will start. If unspecified, it will start at the current epoch.

preliminary_end_epoch

Option<u64>

The epoch at which the farm should preliminarily end (if it's not expanded). If unspecified, the farm will default to end at 14 epochs from the current one.

curve

Option<Curve>

The type of distribution curve. If unspecified, the distribution will be linear.

farm_asset

Coin

The asset to be distributed in this farm.

farm_identifier

Option<String>

If set, it will be used to identify the farm.

ManagePosition

Manages a position based on the action, which can be:

  • Create: Creates a position.

  • Expand: Expands a position.

  • Close: Closes an existing position. The position stops earning farm rewards.

  • Withdraw: Withdraws the LP tokens from a position after the position has been closed and the unlocking duration has passed.

Positions are stored in the POSITIONS map.

{
  "manage_position": {
    "action": {
      "create": {
        "identifier": "position_identifier",
        "unlocking_duration": 86400,
        "receiver": "mantra1..."
      }
    }
  }
}
KeyTypeDescription

identifier

Option<String>

The identifier of the position.

unlocking_duration

u64

The time it takes in seconds to unlock this position.

receiver

Option<String>

The receiver for the position. If left empty, defaults to the message sender.

Claim

Claims the rewards for the user.

{
  "claim": {}
}

UpdateConfig

Updates the contract configuration.

{
  "update_config": {
    "fee_collector_addr": "mantra1...",
    "epoch_manager_addr": "mantra1...",
    "pool_manager_addr": "mantra1...",
    "create_farm_fee": {
      "denom": "uom",
      "amount": "1000000000"
    },
    "max_concurrent_farms": 7,
    "max_farm_epoch_buffer": 14,
    "min_unlocking_duration": 86400,
    "max_unlocking_duration": 31536000,
    "farm_expiration_time": 2629746,
    "emergency_unlock_penalty": "0.01"
  }
}
KeyTypeDescription

fee_collector_addr

Option<String>

The address to of the fee collector, to send fees to.

epoch_manager_addr

Option<String>

The epoch manager address, where the epochs are managed.

pool_manager_addr

Option<String>

The pool manager address.

create_farm_fee

Option<Coin>

The fee that must be paid to create a farm.

max_concurrent_farms

Option<u32>

The maximum amount of farms that can exist for a single LP token at a time.

max_farm_epoch_buffer

Option<u32>

The maximum amount of epochs in the future a new farm is allowed to start in.

min_unlocking_duration

Option<u64>

The minimum amount of time that a user can lock their tokens for. In seconds.

max_unlocking_duration

Option<u64>

The maximum amount of time that a user can lock their tokens for. In seconds.

farm_expiration_time

Option<u64>

The amount of time after which a farm is considered to be expired after it ended. In seconds. Minimum a month. Once a farm is expired it cannot be expanded, and expired farms can be closed.

emergency_unlock_penalty

Option<Decimal>

The penalty for unlocking a position before the unlocking duration finishes. In percentage.

UpdateOwnership(::cw_ownable::Action)

Implements cw_ownable. Updates the contract's ownership. ::cw_ownable::Action can be TransferOwnership, AcceptOwnership and RenounceOwnership.

Note: This is a cw_ownable message.

Propose to transfer the contract's ownership to another account, optionally with an expiry time. Can only be called by the contract's current owner. Any existing pending ownership transfer is overwritten.

{
  "update_ownership": {
    "transfer_ownership": {
      "new_owner": "mantra1...",
      "expiry": {
        "at_height": "424242424242"
      }
    }
  }
}
KeyTypeDescription

new_owner

String

The new owner proposed,

expiry

Option<Expiration>

Optional expiration time parameter.

QueryMsg

Config

Returns the configuration of the contract.

{
  "config": {}
}

Farms

Retrieves the configuration of the manager.

{
  "farms": {
    "filter_by": {
      "identifier": "farm_identifier"
    },
    "start_after": "identifier_123",
    "limit": 30
  }
}
KeyTypeDescription

filter_by

Option<FarmsBy>

An optional parameter specifying what to filter farms by. Can be either the farm identifier, lp denom or the farm asset.

start_after

Option<String>

An optional parameter specifying what farm (identifier) to start searching after.

limit

Option<u32>

The amount of farms to return. If unspecified, will default to a value specified by the contract.

Positions

Retrieves the positions for an address.

{
  "positions": {
    "filter_by": {
      "identifier": "position_identifier"
    },
    "open_state": true,
    "start_after": "identifier_123",
    "limit": 30
  }
}
KeyTypeDescription

filter_by

Option<PositionBy>

An optional parameter specifying what to filter positions by. Can be either the position identifier or receiver.

open_state

Option<bool>

An optional parameter specifying to return only positions that match the given open state. If true, it will return open positions. If false, it will return closed positions.

start_after

Option<String>

An optional parameter specifying what position (identifier) to start searching after.

limit

Option<u32>

The amount of positions to return. If unspecified, will default to a value specified by the contract.

Rewards

Retrieves the rewards for an address.

{
  "rewards": {
    "address": "mantra1..."
  }
}
KeyTypeDescription

address

String

The address to get all the farm rewards for.

LpWeight

Retrieves the total LP weight in the contract for a given denom on a given epoch.

{
  "lp_weight": {
    "address": "mantra1...",
    "denom": "uom",
    "epoch_id": 50
  }
}
KeyTypeDescription

address

String

The address to get the LP weight for.

denom

String

The denom to get the total LP weight for.

epoch_id

u64

The epoch id to get the LP weight for.

Ownership

Returns the ownership of the contract.

Note: This is a cw_ownable query.

{
  "ownership": {}
}

MigrateMsg

Message to migrate the contract to a new code ID.

{}

Last updated