Skip to main content

How strategy work

Majora blocks

Definition

A block is a standardized smart contract that performs an atomic action on a DeFi protocol. For each protocol integration in Majora, there will be one block per protocol interaction.

As exemple, for Curve protocol integration, we have:

  • Deposit Block: This smart contract will deposit asset deposited in the vault and deposit them in a curve pool and withdraw them during a withdrawal
  • Stake Block: This smart contract will deposit Curve LPs owned by the vault in a Curve gauge and withdraw them during a withdrawal
  • Claim Block: This smart contract will claim CRVs from the distribution contract on harvest operation

In fact, there is two types of blocks:

  • Strategy Blocks are implemented to be executed during vault rebalance operation
  • Harvest Blocks are implemented to be executed during a vault harvest operation

This blocks have two commons functions:

ipfsHash

This function return an IPFS hash that contain block's metadata

/// @notice The IPFS hash of the block metadata file
function ipfsHash() external view returns (string memory);

dynamicParamsInfo

This function return the dynamic params needed by the block. This function is used by MajoraDataAggregator to aggregate all the parameteres needed by a strategy to be rebalanced or harvested

/// @notice Return dynamic parameters needed for the block execution
/// @param _exec The type of block execution
/// @param _parameters The parameters of the block execution
/// @param _oracleState The state of the oracle
/// @param _percent The percentage of the block execution
/// @return dynParamsNeeded Whether the strategy block execution needs dynamic parameters
/// @return dynParamsType The type of dynamic parameters needed
/// @return dynParamsInfo The information of the dynamic parameters needed
function dynamicParamsInfo(
DataTypes.BlockExecutionType _exec,
bytes memory _parameters,
DataTypes.OracleState memory _oracleState,
uint256 _percent
) external view returns (bool, DataTypes.DynamicParamsType, bytes memory);

Dynamic parameters

Vault strategy can be operated with dynamic parameters that adjust their behavior based on external inputs. These parameters can be passed during operations (e.g rebalancing/harvesting) to adapt the strategy to current market conditions or specific operational needs. There are different types of dynamic parameters.

Portal Swap

Dynamic swap parameters can be used in blocks to define a token swap requirement via the Strateg Portal Module. The block can provide a token swap operation dynamically during the execution of a block. These parameters can be adjusted based on the current state, enabling the block to perform optimized token swaps based on current market conditions or specific block requirements.

/// @notice Enum representing the different types of swap value
enum SwapValueType {
INPUT_STRICT_VALUE,
INPUT_PERCENT_VALUE,
OUTPUT_STRICT_VALUE
}

/// @notice Struct representing the dynamic swap parameters
/// @param fromToken The address of the token to swap from
/// @param toToken The address of the token to swap to
/// @param value The amount of tokens to swap
/// @param valueType The type of value to swap
struct DynamicSwapParams {
address fromToken;
address toToken;
uint256 value;
SwapValueType valueType;
}

/// @notice Struct representing the dynamic swap data
/// @param route The route to use for the swap
/// @param sourceAsset The address of the asset to swap from
/// @param approvalAddress The address to approve for the swap
/// @param targetAsset The address of the asset to swap to
/// @param amount The amount of tokens to swap
/// @param data The data to send to the swap
struct DynamicSwapData {
uint8 route;
address sourceAsset;
address approvalAddress;
address targetAsset;
uint256 amount;
bytes data;
}

For exemple, if the block need to execute a swap, it must return the following data on the dynamicParamsInfo function call:

return true, DataTypes.DynamicParamsType.PORTAL_SWAP, DynamicSwapParams({
fromToken: <tokenIn address>,
toToken: <tokenOut address>,
value: <amount>,
valueType: <Type of the value provided>
})

Static call

This dynamic parameters will ask to the operator to request an eth_call on the EVM RPC on the provided address with the specified data. It can be useful if the block need a protection against market manipulation by comparing call data with the data during the execution.

/// @notice Struct representing the static call parameters
/// @param to The address of the contract to call
/// @param data The data to send to the contract
struct StaticCallParams {
address to;
bytes data;
}

Merkle

This dynamic parameters doesn't need to return data on dynamicParamsInfo call. The operators will call the merkle API to check if the vault address have pending rewards and provided merkle proof to claim them to the block.

Strategy blocks

This blocks are executed by the vault during vault rebalance operation by a deleguate call. This operation happend when the buffer vault size has derived from it's size.

There is two different execution context in strategy blocks: enter and exit

Each context have a function with the same name and an oracle function.

The enter context:

The enter function take in parameter the block index in the strategy to retrieve its storage pointer.

/// @notice Execute enter block function 
/// @param _index The index of the block
function enter(uint256 _index) external;

The oracleEnter function take in parameter an OracleState that containing a tokens inventory and its parameters. This function have to simulate the enter function and return the OracleState updated after the simulation.

As exemple, for the curve Deposit block in 3CRV pool, we will have 100 USDT as input for enter context, it will return 0 USDT and 95 3CRV

This function allow the MajoraDataAggregator contract to chain oracleEnter of all blocks that composed a strategy to simulate the enter execution

/// @notice Execute the oracle enter function
/// @param previous The previous state of the oracle
/// @param parameters The parameters of the oracle enter
/// @return The new state of the oracle
function oracleEnter(DataTypes.OracleState memory previous, bytes memory parameters)
external
view
returns (DataTypes.OracleState memory);

The exit context:

The exit function take in parameter the block index in the strategy to retrieve its storage pointer and the percentage of fund to exit.

/// @notice Execute exit block function
/// @param _index The index of the block
/// @param _percent The percentage of the assets used for the exit execution
function exit(uint256 _index, uint256 _percent) external;

The oracleExit function take in parameter an OracleState that containing a tokens inventory and its parameters. This function have to simulate the exit function and return the OracleState updated after the simulation.

As exemple, for the curve Deposit block in 3CRV pool, we will have 95 3CRV as input for exit context, it will return 100 USDT and 0 3CRV

This function allow the MajoraDataAggregator contract to chain oracleExit of all blocks that composed a strategy to simulate the exit execution This function also be used to calculate the totalAsset of the ERC4626 vault when users deposit/withdraw.

/// @notice Execute the oracle exit function
/// @param previous The previous state of the oracle
/// @param parameters The parameters of the oracle exit
/// @param _percent The percentage of the assets used for the exit execution
/// @return The new state of the oracle
function oracleExit(DataTypes.OracleState memory previous, bytes memory parameters, uint256 _percent)
external
view
returns (DataTypes.OracleState memory);

When the buffer size is over its configured value after a deposit, operators will query the MajoraDataAggregator contract to know get that is needed to execute the rebalance operation.

Harvest block

This blocks are executed by the vault during vault harvest operation by a deleguate call. This operation happend when the buffer vault size has derived from it's size.

There is just one execution context in harvest blocks: harvest

This context have a function harvest and oracleHarvest.

The harvest context:

The harvest function take in parameter the block index in the harvest execution to retrieve its storage pointer.

/// @notice Execute harvest block function 
/// @param _index The index of the block
function harvest(uint256 _index) external;

The oracleHarvest function take in parameter an OracleState that containing a tokens inventory and its parameters. This function have to simulate the harvest function and return the OracleState updated after the simulation.

As exemple, for the curve Claim block in 3CRV pool, we will have 100 USDT as input for harvest context, it will return 0 USDT and 95 3CRV

This function allow the MajoraDataAggregator contract to chain oracleHarvest of all blocks that composed the harvest execution to simulate the harvest

/// @notice Execute the oracle enter function
/// @param previous The previous state of the oracle
/// @param parameters The parameters of the oracle enter
/// @return The new state of the oracle
function oracleEnter(DataTypes.OracleState memory previous, bytes memory parameters)
external
view
returns (DataTypes.OracleState memory);

Block matadatas

Block's metadata files are stored on IPFS. The underlying file must be a JSON file containing block metadata. For example, the deposit on Aave V3 contains the following metadata.

{
"id":"AAVE_V3_DEPOSIT",
"type":"block",
"gitUrl":"https://github.com/majora-finance/aave-v3-blocks",
"resolver":"QmP9yBZwnRcpEh1dqyFDkkha3dGXPgcgaYgjVfvGJzNKT4",
"name":"Aave V3 Deposit Strateg. Block",
"description":"Block to deposit a token on Aave V3",
"action":"Deposit",
"protocolId":"AAVE_V3",
"protocolName":"Aave v3",
"paramsTuple":"tuple(uint256 tokenInPercent, address token)",
"params":[
{
"attribute":"tokenInPercent",
"name":"Amount",
"type":"percent",
"underlyingType":"uint256"
},
{
"attribute":"token",
"name":"Deposit",
"type":"ERC20",
"underlyingType":"address",
"resolved":"true"
}
]
}

There is the description of each entries:

  • id: It corresponds to the unique block identifier for a single chain. It must be composed of the protocol identifier followed by the action in uppercase snakecase.
  • type: There are three possible values for this attribute:
    • block: Define the block as a strategy block. The block contract must be compliant with the StrategStrategyBlock interface.
    • harvest-block: Define the block as a harvest block. The block contract must be compliant with the StrategHarvestBlock interface.
    • position-manager: Define the block as a position manager, which an extension of the StrategStrategyBlock interface. For this type of block, a remote contract from the borrow module must be deployed during the vault deployment process.
  • gitUrl: The link to the public git repository containing smart contracts code.
  • resolver: IPFS hash containing a typescript file executed as a lambda that returns all available configurations of the block.
  • name: Human readable block name.
  • description: Human readable description explaining block action(s).
  • action: Human readable short action description (1 or 2 words).
  • protocolId: Protocol identifier uppercase and camelcase.
  • protocolName: Human readable protocol name.
  • paramsTuple: Tuple of parameters taken by the block when the strategy is defined.
  • params: List of object describing block's parameters for frontend rendering.
info

To learn how to implement blocks, you can visit our Developer Kit documentation

Block registry

Before being used by vaults, blocks have to be registered on the MajoraBlockRegistry contract.

Blocks can be added by the following function

function addBlocks(address[] _blocks, string[] _names) external nonpayable

and remove by this one

function removeBlocks(address[] _blocks) external nonpayable

Strategy definition

A vault strategy is defined by two lists of strateg blocks. A strategy setup in a MajoraVault is done by the setVaultStrategy function of MajoraUserInteractions contract. All these parameters determine the vault executions during its operations.

function setVaultStrategy(
address _vault,
address[] positionManagers,
address[] stratBlocks,
bytes[] stratBlocksParameters,
bool[] isFinalBlock,
address[] harvestBlocks,
bytes[] harvestBlocksParameters
) external;
warning

Strategies are immutable. setVaultStrategy function can be called only one time.

  • positionManagers: List of position managers owned by the vault. These addresses have the ability to call a whitelisted function during a position manager rebalance operation.
  • stratBlocks: Ordered list of strategy block addresses executed during strategy execution.
  • stratBlocksParameters: Ordered list of bytes containing encoded parameters of the block in the same index as the one in the stratBlocks list.
  • isFinalBlock: Ordered list of boolean to set a block in the stratBlocks as the last block. This function is only used in the event of a strategy exit. When a block is a final block, it receives the current exit percentage, otherwise it receives 100% as exit percentage.
  • harvestBlocks: Ordered list of harvest block addresses executed during harvest execution.
  • harvestBlocksParameters: Ordered list of bytes containing encoded parameters of block in the same index in harvestBlocks list

Internal oracle system

When a MajoraVault contract needs to compute its own TVL (e.g deposit/withdraw). It does so by calling the oracleExit functions of the blocks, from last to the first one to get it:

/**
* @dev Internal function to get the native TVL (Total Value Locked) of the vault.
* @return The native TVL of the vault.
*/
function _getNativeTVL() internal view returns (uint256) {
address _asset = asset();

DataTypes.OracleState memory oracleState;
oracleState.vault = address(this);

uint256 _strategyBlocksLength = strategyBlocksLength;
if (_strategyBlocksLength == 0 || !isLive) {
return IERC20(_asset).balanceOf(address(this)) + IERC20(_asset).allowance(buffer, address(this));
} else if (_strategyBlocksLength == 1) {
oracleState =
IStrategStrategyBlock(strategyBlocks[0]).oracleExit(
oracleState,
LibBlock.getStrategyStorageByIndex(0),
10000
);
} else {
uint256 revertedIndex = _strategyBlocksLength - 1;
for (uint256 i = 0; i < _strategyBlocksLength; i++) {
uint256 index = revertedIndex - i;
oracleState = IStrategStrategyBlock(strategyBlocks[index]).oracleExit(
oracleState, LibBlock.getStrategyStorageByIndex(index), 10000
);
}
}

return oracleState.findTokenAmount(_asset) + IERC20(_asset).balanceOf(address(this))
+ IERC20(_asset).allowance(buffer, address(this));
}

From dApp creation graph to vault strategy

overview

From this graph, there is logics applied to generate the strategy payload:

  • Edges loop removal: it remove every edges which is returning on a block already included is the strategy
  • Strategy splitting: split strategy and harvest block in two groups
  • Number formating: convert number inputs to big numbers
  • Percent formating: format percent and adapt them to be apply correctly in the strategy. (Exemple: a 33% / 33% / 33% fork will be result as 33% / 50% / 100% )
  • Set each block without output edges as final.

So in this exemple, the list of strategy block will be composed by:

Strategy enter execution:

  • Aave v3 Deposit with 50% of USDT available
  • Curve Deposit with 100% of remaining USDT
  • Deposit 100% of Curve LP in gauge

Strategy exit execution of x percent:

  • Withdraw x% of Curve LP deposited in gauge
  • Withdraw 100% of Curve LP available
  • Withdraw x% of USDT available in Aave v3

Harvest execution:

  • Claim gauge rewards (CRV)
  • Swap 100% of available CRV to USDT