This developer kit aim to provide tools to implement Majora. Blocks
mkdir myNewBlock
cd myNewBlock
npx hardhat
npm i @majora-finance/developer-kit
Edit your hardhat.config.ts and add
import "@majora-finance/developer-kit/src/tasks";
A block corresponding to an action specific to a DeFi protocol So to integrate a protocol, you´ll have to implement one block per actions doable on this one
By exemple, for our Aave V3 blocks, you can find 3 blocks
- Deposit: Deposit asset on Aave pool
- Borrow: Deposit asset on Aave pool and borrow a dept at a specified healthfactor
- Leverage: Deposit asset on Aave pool and leverage at a specified healthfactor with dept swapped and deposited as collateral
You can have a look on Aave v3 blocks here
There is two types of block: strategy block and harvest block
There is two functions in common between every block present in IMajoraCommonBlock interface:
interface IMajoraCommonBlock {
function ipfsHash() external view returns (string memory);
function dynamicParamsInfo(
DataTypes.BlockExecutionType _exec,
bytes memory parameters,
DataTypes.OracleState memory oracleState
) external view returns (bool, DataTypes.DynamicParamsType, bytes memory);
}
The function ipfsHash
have to return the hash of an IPFS document where the blocks metadatas are stores and dynamicParamsInfo
function have to return if the block needs dynamic parameters provided by operators on execution (exemple: Swap parameters). To learn more about dynamic parameters, have a look here
The strategy block is dedicated to be executed when the vault will put funds to work by executing the strategy block list and when it will withdraw funds of the strategy.
This block type need to be complient with our protocol by implementing this interface:
interface IMajoraStrategyBlock is IMajoraCommonBlock {
function enter(uint256 _index) external;
function exit(uint256 _index, uint256 _percent) external;
function oracleEnter(DataTypes.OracleState memory previous, bytes memory parameters)
external
view
returns (DataTypes.OracleState memory);
function oracleExit(DataTypes.OracleState memory previous, bytes memory parameters)
external
view
returns (DataTypes.OracleState memory);
}
The enter
& exit
function are called by the vault with a delegate call. So they are executed with the vault's context. The enter
function have to execute the action for which the block is written for. The exit
function have to the the opposit of the enter function.
This functions receive an index number which allow the block to retrieve its parameters with a storage pointer pointing on a bytes variable. You'll have to write a Struct representing the data you'll have to store inside it. For immutables variables, prefer use the constructor to set them.
The enterOracle
& exitOracle
function are called by the vault to simulate enter
or exit
functions. this functions receive an OracleState
with simulated tokens amounts at the moment of the block execution. The goal is to edit it like will do the execution function. The bytes parameters is the same block parameters than received on the corresponding execution function.
By exemple, for a Aave deposit block, the enter function have to deposit a token in the lending pool and the exit function have to withdraw funds. For the oracle functions, the enterOracle
will remove the deposited $TKN and add the $aTKN amount.
There is many function in OracleState library to help to manage it:
- findTokenAmount
- addTokenAmount
- setTokenAmount
- removeTokenAmount
- removeTokenPercent
- removeAllTokenPercent
You can found the library here
Harvest block is based on the same concept than strategy block, the harvest block list is executed on vault harvest.
The oracleHarvest
is a non view function to be usable with Curve like claims on which there is a need to update a local variable.
interface IMajoraStrategyBlock is IMajoraCommonBlock {
function harvest(uint256 _index) external;
function oracleHarvest(DataTypes.OracleState memory previous, bytes memory parameters)
external
returns (DataTypes.OracleState memory);
}
When your block smart contract is ready, you'll have to write contract NatSpec on the contract to generate metadata template with the developer kit.
This file will be used to index the block on our backend and retrive all the information of the block on our frontend.
NatSpec exemple:
/**
* @title Aave V3 Deposit Majora. Block
* @author Bliiitz
* @notice Block to deposit a token on Aave V3
* @custom:block-id AAVE_V3_DEPOSIT
* @custom:block-type block
* @custom:block-action Deposit
* @custom:block-protocol-id AAVE_V3
* @custom:block-protocol-name Aave v3
* @custom:block-params-tuple tuple(uint256 tokenInPercent, address token)
*/
contract MajoraAaveV3DepositBlock is IMajoraStrategyBlock {
After have written this natspec you can call the hardhat task to generate YAML files.
npx hardhat generate-block-metadata
It will create the blocks-metadata
folder ad create one metadata file per contract.
Yaml exemple:
id: AAVE_V3_DEPOSIT
name: Aave V3 Deposit Majora. Block
description: Block to deposit a token on Aave V3
type: block
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
resolver: AaveV3Deposit
Here, the params object is dedicated to the backend / frontend.
- Attribute: tuple attribute name
- Name: the name of the input on the block configuration modal
- Type: the type of input
- Resolver: a lambda like function which will be executed to retrieve the data
- Underlying type: low level type
- Type
ERC20
: display a list of ERC20 tokens returned by the resolver - Type
percent
: percent in basis point (10000 = 100%) - Type
1e18
: display a number input and add a 10^18 multiplier on user input after validation - Type
select
: display a select based on a list of possibility returned by the resolver or thevalues
parameter attribute - Type
value
: use the value by the specified resolver or thevalue
parameter attribute
If you set types as solidity type (uin256 / address / etc) without resolver, an input of corresponding type will be display on the vault configuration
- Type
address
: display a simple text input - Type
bytes
: display a simple text input - Type
uint256
: display a simple text input - Type
bool
: display a simple checkbox input
generate yaml config with :
npx hardhat generate-block-metadata
First, write your block smart contracts following the step mentionned above and complete the blocks metadata yaml files.
Generate the Dev-kit configuration by running the following command:
npx hardhat generate-devkit-config
This command will create YAML files in the devkit-config
directory and corresponding tests in the tests/
directory.
For each YAML file, you need to specify the following parameters:
-
blockParameters
: Configuration parameters specific to each block. Ensure the order of these parameters matches the exact sequence outlined in the Solidity contract. -
constructorArgs
: Arguments required for the constructor within the block. -
tokenIN
information:-
Holder
: Address of a wallet that owns thetokenIN
on mainnet. This simplifies the process as there's no need to mint or acquire tokens manually. -
Amount
: Amount oftokenIN
that the holder should have. -
Decimals
: Number of decimals thetokenIN
uses.
-
To run the tests, execute the generated tests, which are named using the pattern DevKit + BlockName + test.ts
, using this command :
npx hardhat test test/Devkit<BlockName>.test.ts
Each test corresponds to a YAML configuration file and tests the block as configured.