Locked Farming Positions

Open a Locked Farming Position

The openPosition function is used to open a new farming position. This function requires as input the FarmingPositionRequest struct.

At the moment of creation, a positionID that represents the position is generated:

positionId = uint256(keccak256(abi.encode(uniqueOwner,block.number, request.setupIndex)));

Each id is generated to encode the uniqueOwner(owner's position) address, the block number (current block at the time of opening position) and the setupIndex (index of the chosen setup) in a locked setup. This allows an address to correspond to multiple positions if created in different blocks in a locked setup.

The liquidity is added to the AMM calling internally the function _addLiquidity, which directly uses the on-chain API addLiquidity provided by the AMM Aggregator to perform the operations:

(LiquidityPoolData memory liquidityPoolData, uint256 mainTokenAmount) = _addLiquidity(request.setupIndex, request);

In a locked setup, unlike a free setup, it is possible to calculate in advance the fixed reward for a position. It depends on how much liquidity staked in the position as the main token in relation to the setup's maxSteakable amount. At the moment of staking, the rewards distribution for the position from the opening block until the end block is calculated and saved in position memory.

(reward, lockedRewardPerBlock) = calculateLockedFarmingReward(request.setupIndex, mainTokenAmount, false, 0);

A peculiar feature of locked setups is the management of a farmer's liquidity. When the farmer stakes a position in a locked setup, a corresponding amount of Farm tokens (fLP) are minted. The amount minted is exactly equal (1:1 ratio) to the amount of LP tokens of the position. fLP are native Items, and are minted and then sent directly to the farmer using _mintFarmTokenAmount function:

_mintFarmTokenAmount(uniqueOwner, liquidityPoolData.amount, request.setupIndex);

Each locked setup corresponds to a different Farm token, so the first user who stakes a position in one creates its native collection of Farm token references.

At this point, the FarmingPosition struct relative to the created position is populated as follow:

_positions[positionId] = FarmingPosition({ uniqueOwner: uniqueOwner, setupIndex : request.setupIndex, liquidityPoolTokenAmount: liquidityPoolData.amount, mainTokenAmount: mainTokenAmount, reward: reward, lockedRewardPerBlock: lockedRewardPerBlock, creationBlock: block.number

The FarmingPosition struct is used after the position creation inside a farming setup, so first the FarmingPositionRequest is used and only after that also the FarmingPosition.

Fundamental to locked setups is that the amount of liquidity staked when a position is opened is saved in the position. However, the use of Farm tokens implies that the position is not linked to liquidity management. For example, a user can transfer his Farm tokens and remain with a position linked only to the reward of the position itself. In the same way, a user who has not opened a position in the setup but has an amount of Farm tokens can withdraw the corresponding liquidity in the manner explained below.

Creating a locked position increases the number of positions within that specific setup by 1 + LP amount, given by:

mapping(uint256 => uint256) private _setupPositionsCount;
 _setupPositionsCount[request.setupIndex] += (1 + liquidityPoolData.amount);

So unlike in Free setups, the _setupPositionsCount in a Locked setup (as liquidity management through Farm Tokens is "separated" from the position) is one from the actual position (linked to the reward) and more from the LP token amount, because the amount of LP corresponds to the amount of Farm tokens. Therefore a Farm token holder may not even be the owner of the position.

Add Liquidity to an Existing Position

In a locked setup position no further liquidity can be added to an already open position.

Withdraw reward

To withdraw the position reward the withdrawReward function is used. The withdrawReward can be called only by the position owner and not by who owns only Farm token without having a position.

This function requires as input:

function withdrawReward(uint256 positionId) public byPositionOwner(positionId) 
  • positionId -> id corresponding to the position to be transferred

If there is a claimable reward amount, the function transfer the amount to the position owner address using the _safeTransfer function if ETH is not involved as reward token or uses the call value if ETH is involved:

if (_rewardTokenAddress != address(0)) {
    _safeTransfer(_rewardTokenAddress, farmingPosition.uniqueOwner, reward);
} else {
    (bool result,) = farmingPosition.uniqueOwner.call{value:reward}("");
    require(result, "Invalid ETH transfer.");
}

the withdrawn reward quantity is removed from the farming position total reward:

farmingPosition.reward = currentBlock >= _setups[farmingPosition.setupIndex].endBlock ? 0 : farmingPosition.reward - reward;
farmingPosition.creationBlock = block.number;

N.B the withdrawReward can be called directly and is not internally called when the withdrawLiquidity function is used as in a free setup.

After the setup endBlock, if the reward withdrawn amount is equal to the entire amount of reward of the position (and therefore the remaining reward is 0) the position is automatically deleted from the setup and from the contract memory:

delete _setups[farmingPosition.setupIndex];

In this case the _setupPositionsCount is decreased by 1

Unlock

To withdraw the position liquidity and to close the position during the setup period the unlock function is used. This function can only be called if the setup end block has not been reached. This function requires as input:

function unlock(uint256 positionId, bool unwrapPair) public payable byPositionOwner(positionId) 
  • positionId -> id corresponding to the position to be transferred

  • unwrapPair -> a boolean value representing if the required liquidity to be withdrawn is wanted in LP (true) or in pairs token (false)

In order to unlock a position it is necessary to give back to the treasury all the partial reward that has been withdrawn up to that moment. So if the position Owner has never withdrawn rewards until the moment of unlocking, he will not have to return anything. The _giveBack function takes care of this:

 if (rewardToGiveBack > 0) {
     _safeTransferFrom(_rewardTokenAddress, msg.sender, address(this), rewardToGiveBack);
     _giveBack(rewardToGiveBack);
} 

To unlock the position it is necessary to have a quantity of Farm token at least equal to the quantity of LP token inserted during the openPosition and saved in memory as farmingPosition.liquidityPoolTokenAmount . So the unlock function cane be calle only by a position owner who owns a quantity of Farm token at least equal to the quantity of LP token linked to the position.

The amount of Farm token sent is burned by the function calling internally the _burnFarmTokenAmount function considering the Farm token objectId of the locked setup and the farmingPosition.liquidityPoolTokenAmount.

_burnFarmTokenAmount(_setups[farmingPosition.setupIndex].objectId, farmingPosition.liquidityPoolTokenAmount);

Then to withdraw liquidity from the AMM, the _removedLiquidity function is called internally that acts as a facilitator. The inputs parameters are the same of withdrawLiquidity function (the isUnlock parameter is true because you're using the unlock function).

function _removeLiquidity(uint256 positionId, uint256 setupIndex, bool unwrapPair, uint256 removedLiquidity, bool isUnlock) private

If unwrapPair is true then a _safeApprove is executed and the removeLiquidity on the AMM using the methods of the AMM Aggregator to withdraw the liquidity sending to the msg.sender as pairs token.

if (unwrapPair) {
    _safeApprove(lpData.liquidityPoolAddress, setupInfo.ammPlugin, lpData.amount);
    IAMM(setupInfo.ammPlugin).removeLiquidity(lpData);

If unwrapPair is false then LP token amount is sent back to the msg.sender (position owner) using the _safeTransfer method:

_safeTransfer(lpData.liquidityPoolAddress, lpData.receiver, lpData.amount);

In this case the _setupPositionsCount is decreased by 1 + farmingPosition.liquidityPoolTokenAmount

Withdraw Liquidity

To withdraw the position liquidity the withdrawLiquidity function is used. This function can only be called if the setup end block has been reached. This function requires as input:

function withdrawLiquidity(uint256 positionId, uint256 objectId, bool unwrapPair, uint256 removedLiquidity) public 
  • positionId ->a positionId value equal to 0 is passed in the case of withdrawLiquidity calling

  • objectId -> Farm token object id relatives to the specific setup

  • unwrapPair -> a boolean value representing if the required liquidity to be withdrawn is wanted in LP (true) or in pairs token (false)

  • removedLiquidity -> the amount of liquidity to be removed.

Therefore in a locked setup it is possible to remove (after the end block has been reached) any amount of liquidity equal to the amount of Farm token owned. This means that a quantity of liquidity greater or less than the liquidity inserted at the moment of opening the position can be removed but also that liquidity can also be withdrawn from a Farm token holder who owns only the tokens but has never opened a farming position in the setup. The liquidity is linked to the amount of Farm tokens held and not to the liquidity stored in the position memory when a new position is opened. So the withdrawLiquidity function can be called also by a Farm token holder who not owns also the position (PositionOwner)

The amount of Farm token sent is burned by the function calling internally the _burnFarmTokenAmount function:

if (positionId == 0) {
    _burnFarmTokenAmount(objectId, removedLiquidity);
}

Then to withdraw liquidity from the AMM, the _removedLiquidity function is called internally that acts as a facilitator. The inputs parameters are the same of withdrawLiquidity function (the isUnlock parameter is an unpopulated parameter in a locked withdrawLiquidity calling)

function _removeLiquidity(uint256 positionId, uint256 setupIndex, bool unwrapPair, uint256 removedLiquidity, bool isUnlock) private

If unwrapPair is true then a _safeApprove is executed and the removeLiquidity on the AMM using the methods of the AMM Aggregator to withdraw the liquidity sending to the msg.sender as pairs token

if (unwrapPair) {
    _safeApprove(lpData.liquidityPoolAddress, setupInfo.ammPlugin, lpData.amount);
    IAMM(setupInfo.ammPlugin).removeLiquidity(lpData);

If unwrapPair is false then LP token amount is sent back to the msg.sender (position owner) using the _safeTransfer method:

_safeTransfer(lpData.liquidityPoolAddress, lpData.receiver, lpData.amount);

N.B As said in the previous point, the withdrawLiquidity function is not internally called when the withdrawLiquidity function is used as in a locked setup.

In this case the _setupPositionsCount is decreased by farmingPosition.liquidityPoolTokenAmount

When the last position is closed and the setup is inactive (look at the Activate/Disactivate farming setup section ) then the entire setup is deleted from the memory of the contract.

Last updated