At its core, Mangrove is an order book decentralized exchange where liquidity is not locked and a "smart offer strategy" dictates how that liquidity behaves. Unlike traditional DEXs that lock up liquidity, Mangrove's offers are essentially promises to trade, allowing Makers(liquidity providers) to build custom strategies. For ex- A Maker can keep their assets productive in other protocols and when Taker comes to take the prommised offer, Maker's liqudity will be sourced Just-in-Time from wherever it is stored. Kandel is another powerfull strategy discussed later. All of this is possible because of "smart offers" which allows arbitrary smart contract code to be attached to an offer.
Offers are organized into offer lists, with each market having two lists:
- Asks Offer List: Where offers sell the base token (token0) (Taker provides the quote token (token1))
- Bids Offer List: Where offers sell the quote token (token1) (Taker provides the base token (token0))
In this example USDC/WETH market is implemented i.e token0 as USDC and token1 as WETH.
Within an offer list, offers are grouped by ticks, which represent discrete price levels. The price is derived from the tick, and offers at the same tick are executed in a First-In, First-Out order.
With smart offers, Makers can include defensive code to cancel a trade if market conditions have become unsatisfactory. But what if everyone makes empty promises, and the offers in the book are all meant to fail?
A critical component of the Mangrove engine is the provision system, which addresses the potential issue of "empty promises" offers that are posted but are intended to fail. To ensure that the offers on the order book are credible, Makers must deposit a provision in the native token. If an offer fails to execute when a Taker attempts to fill it, a portion of this provision, known as the bounty, is paid to the Taker as compensation for the wasted gas fees. This creates a financial disincentive for Makers to post frivolous or unreliable offers.
The required provision amount is calculated using the following formula:
Parameters:
-
$\text{gasprice}_{\text{mgv}}$ : The global governancegaspriceparameter (in Mwei per gas unit) -
$\text{gasprice}_{\text{ofr}}$ : Thegaspriceargument passed tonewOfferorupdateOfferfunctions (in Mwei per gas unit) -
$\text{gasreq}$ : The amount of gas units required to execute the offer -
$\text{gasbase}_{\text{mgv}}$ : The local governanceoffer_gasbaseparameter
Kandel is smart offer strategy that functions like a bot. Its primary goal is to buy low and sell high within a user-defined price range, generating profit from the accumulated spread between the buy (Bid) and sell (Ask) offers that are filled. It achieves this by instantly reposting offers based on the on-chain order flow.
Kandel's core logic is a continuous cycle of buying, selling, and moving liquidity across the price grid. This process is automatic and reactive to trades as they occur.
Scenario A: A Bid is Taken (Buying Low)
When a trader takes one of Kandel's bids, the strategy contract sends the agreed-upon amount of quote tokens (WETH in our implementation) and receives the base tokens (USDC).
The newly acquired base tokens are then immediately used to post a new Ask offer at a higher price point on the grid (specifically, one step above the bid that was just filled). This new offer is referred to as a "dual offer".
Scenario B: An Ask is Taken (Selling High)
Conversely, when one of Kandel's asks is taken, the contract sends the base tokens (USDC) and receives the quote tokens (ETH).
The newly received quote tokens are then used to post a new Bid offer at a lower price point on the grid.
This dynamic ensures that as the market price moves, Kandel continuously repositions its liquidity(upto Price point) to maintain a series of bids below the market price and asks above it, always ready to trade.
Profit is generated from the spread-difference in price between where Kandel buys and where it sells. This profit is automatically compounded back into the strategy.
Profit Calculation: The profit is the difference between the quote tokens received from an Ask and the quote tokens spent on its corresponding Bid. For example lets take an ETH/USDC market type for better understanding, if Kandel spends 3000 USDC to buy 1 ETH (a bid) and later sells that 1 ETH for 3050 USDC (an ask), the profit is 50 USDC.
Automatic Reinvestment: This profit is not held separately; it is immediately reinvested. When the strategy posts the new bid after selling high, it includes the profit. In the example above, the new bid would be for a larger amount (e.g., 3050 USDC), effectively increasing the capital working in the strategy and compounding the returns over time.
The application is built using modern web technologies:
- Frontend Framework: Next.js 14 with TypeScript
- Blockchain Integration: wagmi 2.0 + viem 2.0 for Web3 interactions
- Styling: Tailwind CSS for responsive design
- State Management: React Query (@tanstack/react-query) for efficient data fetching
- Onchian Interaction: Smart Contracts and @mangrovedao/mgv library for Kandel-specific utilities
Implementation Location: src/components/WalletConnect.tsx, src/utils/config.ts
The wallet connection system successfully integrates with:
- Supported Wallets: MetaMask, Injected wallets
- Network Configuration: Local Anvil instance (chainId 31337) at
http://127.0.0.1:8545 - Hydration Handling: Proper SSR/client-side rendering to prevent hydration mismatches
Implementation Location: src/components/OrderBook.tsx, src/hooks/useMangrove.ts
The order book component provides comprehensive market visualization:
Market Data Fetching:
- Fetches all open markets using
MgvReader.openMarkets() - Real-time offer lists for both bid and ask sides with proper ticks to price conversion using mgv tick library.
User Offer Highlighting:
- Blue highlighting for user's Kandel offers in the order book
- Real-time verification of offer ownership via
offer.maker === kandelAddress
Implementation Location: src/components/KandelPosition.tsx, src/hooks/useKandelPositionCreation.ts
The creation process is a two-transaction sequence:
- Deploy: A call is made to the
KandelSeedercontract to deploy a new, un-initialized Kandel contract instance. - Populate & Fund: A second call is made to the newly created Kandel contract's address. This call configures all the strategy's parameters, publishes the offers to the order book, and transfers the user's initial token inventory and the required native token gas provision into the contract.
To create a seamless user experience, the DApp uses the off-chain helper library to provide real-time feedback to the user as they input their parameters. This process is broken into two phases.
Phase 1: Defining the Strategy Shape First, the user fills in the parameters that define the geometry and structure of their Kandel strategy.
- Market: The user selects a market (e.g., USDC/WETH) and DApp provides the corresponding
basetoken,quotetoken, andtickSpacing. - Price Range: The user provides a
minPriceandmaxPrice. - Mid Price: The user provides the
midPrice, which is the current price to center the distribution. - pricePoints: The user specifies the number of offers for their strategy.
- Step Size: The user provides the
stepSize.
Off-Chain Calculation of Minimums
As soon as the user completes Phase 1, DApp immediately calls the validateKandelParams function from @mangrovedao/mgv
- Action: Call
validateKandelParamswith all the parameters from ui and other paramaeters as shown in the below code snippet. - Purpose: The goal of this initial call is to extract the calculated minimums and the required gas provision from the return object.
- Result: DApp receives the
minBaseAmount,minQuoteAmount, andminProvisionfrom the returnedValidateParamsResultobject.
// Phase 1: Call validateKandelParams to get minimums
const positionParams: RawKandelPositionParams = {
market: setup.market,
minPrice: parseFloat(minPrice),
maxPrice: parseFloat(maxPrice),
midPrice: parseFloat(midPrice),
pricePoints: BigInt(pricePoints),
adjust: true //to find the closest price match
} as any
const phase1ValidationParams: RawKandelParams = {
...positionParams,
baseAmount: BigInt(0), // Mock amount to get minimums
quoteAmount: BigInt(0), // Mock amount to get minimums
stepSize: BigInt(stepSize),
gasreq: 121413n, //As per docs
factor: 1, // 100% of minVolume, can be set to 1.n for a n% buffer.
asksLocalConfig: setup.asksLocalConfigData,// fetched from config01 from mgvReader marketConfig function
bidsLocalConfig: setup.bidsLocalConfigData, // fetched from config10 from mgvReader marketConfig function
marketConfig: setup._globalConfig, // fetched from mgvReader globalUnpacked function containing gasprice which is modified to 4*gasprice
} as any
const result = validateKandelParams(phase1ValidationParams)Phase 2: Funding the Strategy with Real-Time Guidance The UI now presents the input boxes for the initial inventory, enhanced with the data calculated above.
- Initial Inventory: The input fields for
baseAmountandquoteAmountdisplay "Minimum required: [minBaseAmount]" and "Minimum required: [minQuoteAmount]" respectively. - Gas Provision: The UI displays the user required Provision (minProvision).
Final Validation
As the user enters their desired baseAmount and quoteAmount, the DApp calls validateKandelParams again in real-time.
- Action: Call
validateKandelParamswith the complete set of parameters, including the user's actual inventory inputs. - Purpose: This second call is used to check the
isValidboolean flag from the returnedValidateParamsResultobject. - Result:
- If
isValidisfalse, the UI can show an error (e.g., "Base amount is below the required minimum") and keep the submission button disabled. - If
isValidistrue, error messages are cleared, and the "Create Position" button is enabled. The final, validValidateParamsResultobject is stored and ready for the on-chain steps.
- If
This step deploys the empty Kandel instance.
- Action: DApp will call the
sowfunction on the mainKandelSeedercontract. - Arguments:
olKeyBaseQuote: TheOLKeystruct representing the market.liquiditySharing: Abool, which is set tofalsefor a standard user position.
- Result: This transaction returns the
addressof the new Kandel contract. DApp captures this address for the next step.
With the new Kandel address, the user's strategy you is initialized and funded.
-
Approvals: Before calling the populate function, DApp prompts the user to sign
approvetransactions for thebaseAmountandquoteAmount. -
Action: Once the approvals are confirmed, DApp calls the
populateFromOffsetfunction on the new Kandel contract address obtained in Step 2. -
Arguments: The arguments are sourced directly from the final
ValidateParamsResultobject generated in Step 1. -
Confirmation: Once this final transaction is successfully mined, the user's Kandel position is fully deployed and active. The contract will have pulled the user's funds, stored the provision, and published the offers to the Mangrove order book.
Implementation Location: src/components/KandelPositionView.tsx, src/hooks/useKandelManager.ts
Data Fetching Strategy:
Uses efficient multicall pattern via useReadContracts to fetch:
- Kandel parameters (
params()) - Current inventory (
reserveBalance()for both tokens) - Live offer volumes (
offeredVolume()for both sides)
Position State Display:
- Current inventory balances for base and quote tokens
- Live offer count and status
- Gas parameters and step size
Management Operations:
- Deposit Funds: Token approvals +
depositFunds()call - Withdraw Funds:
withdrawFunds()with amount validation - Parameter Modification:
setStepSize(),setGasreq(),setGasprice() - Full Withdrawal:
retractAndWithdraw()for complete position closure
-
Clone the Repository
git clone https://github.com/mo-hak/mangrovelocal-main.git
-
Install Dependencies
# Install root dependencies bun i # Install frontend dependencies cd frontend npm i
-
Run Deploy Script in Root Directory
bun src/index
-
Start the Application
cd frontend npm run dev -
Access the Application
- Open your browser and navigate to
http://localhost:3000 - Connect your wallet (MetaMask recommended)
- Switch to the local network (Chain ID: 31337)(Remeber to add the the anvil account)
- Start creating and managing Kandel positions!
- Open your browser and navigate to
The core principle is to take a snapshot of the total value of the inventory at the beginning and end of a period, using the same price for both calculations. This neutralizes the effect of market price changes and measures only the value generated by the trading strategy.
At the beginning of the measurement period, we would record the initial inventory and establish a reference price.
-
Get Initial Inventory: Query the Kandel contract to find the amount of
baseandquotetokens it holds in its reserve. ThereserveBalancefunction can be used for this.initial_base_inventoryinitial_quote_inventory
-
Set a Reference Price: Determine a fixed
reference_priceto value the inventory. A good option is the mid-price of the Kandel grid at this starting moment. This price should be expressed inquoteperbase. -
Calculate Initial Total Value: Convert the entire inventory's value into a single token term (e.g., its
quotetoken equivalent) using the reference price.initial_total_value = (initial_base_inventory * reference_price) + initial_quote_inventory
At the end of the measurement period (e.g., after 24 hours, 1 week, etc depends on stability of tokens), record the new state of the inventory.
-
Get Final Inventory: Query the
reserveBalancefunction again to get the final token counts.final_base_inventoryfinal_quote_inventory
-
Calculate Final Total Value: Calculate the new total value using the same
reference_pricefrom Step 1. This is the crucial step that isolates the strategy's performance.final_total_value = (final_base_inventory * reference_price) + final_quote_inventory
The difference between the final and initial values represents the profit generated by the spread, which Kandel automatically reinvests.
-
Calculate Earnings:
earnings = final_total_value - initial_total_value
-
Calculate the Return Rate for the period:
period_return_rate = earnings / initial_total_value
-
Annualize the Return (APR):
APR = period_return_rate * (365 / measurement_period_in_days)