The implementation of lending/borrowing operations in Ajna uses two main entry points:
ERC20Pool.sol and
ERC712Pool.sol, containing
drawDebt() functions for borrowing (
ERC20 and
ERC721 implementations) and
addCollateral() functions (
ERC20 and
ERC721 variants). These functions handle only basic checks and token transfers; the main logic of Ajna resides in
LenderActions.sol and
BorrowerActions.sol.
The core lending function, which adds quote tokens to the protocol, is the
addQuoteToken() function from
LenderActions.sol. This function works with a given bucket index and
calculates the required amount of LP tokens for a specified amount of quote tokens (with the deposit fee already
included). Then, global deposits are
updated (note that this is not a simple accumulator value, but an update of the Fenwick tree
here). Then, after saving lenders' LP (
here) goes the calculation of the new LUP (
here), which is used to update the pool state in
updateInterestState() function.
This function, as described in the whitepaper (section 8 "Interest rates"),
calculates EMA ("Exponential Moving Average") values for debts and deposits. While the details of Ajna's mathematical model are not the focus of this article, which is centered on "how it's made", it's important to highlight the key components of Ajna's lending/borrowing mechanics: the use of Fenwick trees for storing "per-bucket" values and the calculation of EMA values. These components play a crucial role in maintaining efficient and accurate tracking of financial metrics within the protocol.
The core borrowing function is located in
Borroweractions.sol and is called
drawDebt(). After performing amount checks and verifying that the debt is not currently being auctioned, the function prepares a
DrawDebtResult struct. This struct contains "pre-" and "post-" operation values for collateral and debt (both for this particular position and the entire pool), as well as the new LUP ("Loan Utilization Price").
The new LUP value, along with the overall pool state, is later used for a "slippage"
check (set by user's
limitIndex_ parameter). This ensures that the new LUP hasn't dropped below a specified limit. Additionally, it is used in the main
_isCollateralized() check (implemented
here) to verify that the position remains correctly collateralized after the operation.
At the end of the borrowing process, the loan for a given borrower is
updated. Here, Ajna employs another interesting solution: a "max heap" data structure. This kind of binary tree maintains a pre-sorted array of values, with the maximum value at the top of the tree.
The loans are sorted within this structure, and each new insert/update of the loan
reorders the max heap tree. This data structure requires reordering when adding/updating/removing (
example) the values, but it allows for the retrieval of the deposit with the highest TP (Threshold Price) by simply
accessing the root index, making the operations involving the highest priority loans efficient.