Mangrove
Developper
Developper
  • Welcome
  • Protocol
    • Introduction
    • Technical References
      • Overview
      • Ticks, ratios, and prices
      • Offer-list
        • Views on offers
      • Market-order
        • Delegation
      • Creating & Updating offers
        • Maker contract
        • Offer provisions
        • Gas requirement
        • Public data structures
        • Executing offers
      • Cleaning offers
      • Governance-parameters
        • Global variables
        • Local variables
        • Data structures and views
      • Periphery Contracts
        • MgvReader
        • MgvOracle
      • Literate Source Code
    • Background
      • Taking available liquidity
      • Making liquidity available
      • Reneging on offers
  • Strat Lib
    • What is the Strat Library?
    • Getting-started
      • Set Up Your Local Environment
      • Post a Smart Offer
    • Guides
      • Unlocking liquidity
      • Reposting an offer in the posthook
      • Using last look to renege trades
      • Determining gas requirements
      • Creating a Direct contract
      • Deploying your contract
      • Testing a maker contract
      • Safe offer logic guidelines
      • Approvals
    • Technical references
      • Principal hooks
      • Liquidity routing
      • API preferences
        • Core
          • SRC
            • IMangrove
        • Strats
          • SRC
            • Strategies
              • MangroveOffer
              • MangroveOrder
              • Integrations
                • AaveV3Borrower
                • AaveV3BorrowerImplementation
                • AaveV3BorrowerStorage
                • AaveV3Lender
                • CompoundModule
              • Interfaces
                • IForwarder
                • ILiquidityProvider
                • IOfferLogic
                • IOrderLogic
              • Offer_forwarder
                • Abstract
                  • Forwarder
              • Offer_maker
                • Abstract
                  • Direct
                • Market_making
                  • Kandel
                    • AaveKandel
                    • AaveKandelSeeder
                    • KandelSeeder
                    • Abstract
                      • AbstractKandelSeeder
                      • CoreKandel
                      • DirectWithBidsAndAsksDistribution
                      • GeometricKandel
                      • HasIndexedBidsAndAsks
                      • KandelLib
                      • TradesBaseQuotePair
              • Routeurs
                • SimpleRouter
                • Abstract
                  • AbstractRouter
                • Integrations
                  • AavePooledRouter
                  • HasAaveBalanceMemoizer
              • Utils
                • AccessControlled
              • Vendor
                • AAVE
                  • V3
                    • Contracts
                      • Dependencies
                        • Oppenzeppelin
                          • Contracts
                            • IERC20
                      • Interfaces
                        • IAToken
                        • IAaveIncentivesController
                        • IAaveOracle
                        • ICreditDelegationToken
                        • IInitializableAToken
                        • IPool
                        • IPoolAddressesProvider
                        • IPriceOracleGetter
                        • IScaledBalanceToken
                      • Protocol
                        • Libraries
                          • Configurations
                            • ReserveConfiguration
                          • Helpers
                            • Errors
                          • Types
                            • DataTypes
                    • Periphery
                      • Contracts
                        • MISC
                          • Interfaces
                            • IEACAggregatorProxy
                        • Rewards
                          • Interfaces
                            • IRewardsController
                            • IRewardsDistributor
                            • ITransferStrategyBase
                          • Libraries
                            • RewardsDataTypes
                • Compound
                  • CarefulMath
                  • Exponential
                  • ExponentialNoError
                  • ICompound
    • Background
      • Building Blocks
        • MangroveOffer
        • Direct
        • Forwarder
  • Vaults
    • Understanding vaults
      • Oracles
    • Managing a vault (CLI)
      • Deploying an oracle
      • Creating a vault
      • Monitoring the vault
      • Setting the vault position
      • Setting the fee data
      • Rebalancing
      • Adding or removing liquidity
    • Custom interactions
      • Oracles
      • Vault Factory
      • Managing a vault
        • Setting the position
        • Rebalancing
        • Setting a manager
        • Setting fee
  • Keeper Bots
    • Keeper Bots
    • Guides
      • Using borrowed funds for cleaning
    • Backgroud
      • The role of cleaning bots in Mangrove
      • The role of gas price updater bots in Mangrove
  • Adresses
    • Deployment Addresses
  • Quick Links
    • Glossary
    • Website
    • Whitepaper
Powered by GitBook
On this page
  • Creating the test file​
  • Imports​
  • Test Contract and Setup​
  • Testing a Successful Fill​
  • Running Tests with Foundry​
  • Adding More Tests - Testing Partial Fills​
  1. Strat Lib
  2. Guides

Testing a maker contract

PreviousDeploying your contractNextSafe offer logic guidelines

Last updated 1 month ago

Work in progress

This page is currently being updated - thank you for your understanding.

After following the tutorial or some of the guides in this section, you have created a and are ready to test it. Mangrove offers a helper contract, , that helps setup everything needed for writing a test using the Mangrove core protocol.

We are going to use as the test runner - and we are going to use some of the features that Foundry provides for testing. Please refer to for instructions on how to get and setup Foundry. For this guide, we assume basic knowledge of writing and running tests with Foundry - as can be gained by reading the relevant sections on in the Foundry book.

For this example, we are going to write a simple test for the Amplifier contract that we implemented in the advanced part of .

Creating the test file

When creating the test file, remember to use the naming convention <name>.t.sol, this way Foundry knows what files are tests.

Imports

The first thing to do is to import the relevant contracts. As mentioned, we are going to use the helper contract provided by the Mangrove test library, . This allows us to avoid having to fork an existing chain with Mangrove deployed. We may simply deploy a Mangrove protocol for this specific purpose before running our tests.

The Mangrove test library provides other helpers. We import Polygon - a helper to fork the polygon chain. We do this because we want to use the real addresses for WETH, USDC and DAI. (This is not strictly necessary - we could just create some test tokens and use those instead.)

We also import the Amplifier contract - the contract that we wish to test. The last thing we import is MgvStructs, which contain struct definitions for the , which we shall need in the test.

The console import is not strictly needed - however, it can be very useful, if we want to log something to the console while we are developing the test. Let us import it for now.

Amplifier.t.sol - Imports

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

import {StratTest} from "@mgv-strats/test/lib/StratTest.sol";
import "@mgv/test/lib/forks/Polygon.sol";
import "@mgv-strats/src/toy_strategies/offer_maker/Amplifier.sol";
import {Local} from "@mgv/src/core/MgvLib.sol";
import {MgvReader} from "@mgv/src/periphery/MgvReader.sol";

import {console} from "@mgv/forge-std/console.sol";

We call the test contract AmplifierTest and let it inherit from MangroveTest to be able to use all helper functions. We override the basic setUp() function provided by MangroveTest to setup the actors we shall need in the test. We need 3 tokens, the fork the test should run on, a taker address and the Amplifier contract.

For this test, we are going to use the test contract as the offer maker. Consequentially, it should be able to receive funds (from the taker), so we make sure to define the receive() function.

We do the following in setUp() - refer to the full implementation below:

  • use the pinned fork of Polygon provided the Mangrove test library

  • get three tokens from the fork

  • setup the two relevant markets - DAI/WETH and USDC/WETH (using a helper from MangroveTest)

  • create an address for the taker, and provide it with native tokens, as well as DAI and USDC for taking the offer(s)

  • as the taker, we also need to approve Mangrove for taking the funds

We also use a helper function $() provided by the Mangrove testing library offering a shorthand for writing address() and casting the contract to its address.

Amplifier.t.sol - Contract and Setup

contract AmplifierTest is StratTest {
  IERC20 weth;
  IERC20 dai;
  IERC20 usdc;

  PolygonFork fork;

  address payable taker;
  Amplifier strat;
  OLKey olKeyWethDai;
  uint constant GASREQ = 200_000;

  receive() external payable virtual {}

  function setUp() public override {
    // use the pinned Polygon fork
    fork = new PinnedPolygonFork(39880000); // use polygon fork to use dai, usdc and weth addresses
    fork.setUp();

    // use convenience helpers to setup Mangrove
    mgv = setupMangrove();
    reader = new MgvReader($(mgv));

    // setup tokens, markets and approve them
    dai = IERC20(fork.get("DAI.e"));
    weth = IERC20(fork.get("WETH.e"));
    usdc = IERC20(fork.get("USDC.e"));
    olKeyWethDai = OLKey($(weth), $(dai), options.defaultTickSpacing);
    olKey = OLKey($(usdc), $(weth), options.defaultTickSpacing);
    lo = olKey.flipped();

    setupMarket(olKeyWethDai);
    setupMarket(olKey);

    // setup separate taker and give some native token (for gas) + USDC and DAI
    taker = freshAddress("taker");
    deal(taker, 10_000_000);

    deal($(usdc), taker, cash(usdc, 10_000));
    deal($(dai), taker, cash(dai, 10_000));

    // approve DAI and USDC on Mangrove for taker
    vm.startPrank(taker);
    dai.approve($(mgv), type(uint).max);
    usdc.approve($(mgv), type(uint).max);
    vm.stopPrank();
  }

With setUp done, let us write the first test.

Breaking the problem down, for this test, we need to

  • deploy Amplifier on our local fork,

  • take one of the offers, and verify our conditions

We implement these two steps as separate functions (we need to deploy the contract for other tests, also).

Amplifier.t.sol - Testing a successful fill

function test_success_fill() public {
  deployStrat();

  execTraderStratWithFillSuccess();
}

Now, of course, we need to implement deployStrat() and execTraderStratWithFillSuccess().

Deploying a contract on the fork is just calling its constructor of the contract. We give the address of the testing contract($(this)) as the administrator of the Amplifier contract to be able to use that directly as the maker.

Amplifier.t.sol - Deploying on a fork

function deployStrat() public {
  strat = new Amplifier({
    mgv: IMangrove($(mgv)),
    base: weth,
    stable1: usdc,
    stable2: dai,
    tickSpacing1: olKey.tickSpacing,
    tickSpacing2: olKeyWethDai.tickSpacing,
    admin: $(this) // for ease, set this contract (will be Test runner) as admin for the strat
  });

  // NOTE:
  // For this test, we're locking base, ie WETH, in the vault of the contract
  // - so Amplifier is not really used for amplified liquidity, in this example.
  // However, to employ actual amplified liquidity it is simply a matter of
  // setting up a more refined router.
  // check that we actually need to activate for the two 'wants' tokens
  IERC20[] memory tokens = new IERC20[](3);
  tokens[0] = dai;
  tokens[1] = usdc;
  tokens[2] = weth;

  vm.expectRevert("mgvOffer/LogicMustApproveMangrove");
  strat.checkList(tokens);

  // and now activate them
  strat.activate(tokens);
}

With this, we have a function that can deploy a new Amplifier contract on our local chain. Let us turn to writing the actual test.

Let us unfold the specification for the test itself.

For this first test, we want to

  • snipe one of the offers, and,

  • verify these properties

    • both maker and taker received tokens as expected

    • after trade execution both offers are retracted

Let us continue our divide-and-conquer strategy and write helper methods for the two first steps, before writing the body of the test. (As we shall see, we can reuse these helper methods, later.)

Amplifier.t.sol - Post and fund offers

function postAndFundOffers(uint makerGivesAmount, uint makerWantsAmountDAI, uint makerWantsAmountUSDC, uint gasreq)
  public
  returns (uint offerId1, uint offerId2)
{
  (offerId1, offerId2) = strat.newAmplifiedOffers{value: 2 ether}({
    gives: makerGivesAmount, // WETH
    wants1: makerWantsAmountUSDC, // USDC
    wants2: makerWantsAmountDAI, // DAI
    gasreq: gasreq
  });
}

Mangrove provides snipe exactly for taking a specific offer.

Amplifier.t.sol - Take a single offer

function takeOffer(uint makerWantsAmount, IERC20 makerWantsToken, uint offerId)
  public
  returns (uint takerGot, uint takerGave, uint bounty)
{
  OLKey memory _olKey = OLKey($(weth), $(makerWantsToken), olKey.tickSpacing);
  Tick tick = mgv.offers(_olKey, offerId).tick();
  // try to take one of the offers (using the separate taker account)
  vm.prank(taker);
  (takerGot, takerGave, bounty,) =
    mgv.marketOrderByTick({olKey: _olKey, maxTick: tick, fillVolume: makerWantsAmount, fillWants: false});
}

With these helper methods, the test body amounts to the following steps:

Amplifier.t.sol - Putting it all together

function execTraderStratWithFillSuccess() public {
  uint makerGivesAmount = 0.15 ether;
  uint makerWantsAmountDAI = cash(dai, 300);
  uint makerWantsAmountUSDC = cash(usdc, 300);

  weth.approve($(strat.router()), type(uint).max);

  deal($(weth), $(this), cash(weth, 10));

  (uint offerId1, uint offerId2) =
    postAndFundOffers(makerGivesAmount, makerWantsAmountDAI, makerWantsAmountUSDC, GASREQ);

  (uint takerGot, uint takerGave,) = takeOffer(makerWantsAmountDAI, dai, offerId1);

  // assert that
  assertEq(takerGot, reader.minusFee(olKeyWethDai.flipped(), makerGivesAmount), "taker got wrong amount");
  assertTrue((makerWantsAmountDAI - takerGave) * 100000 / makerWantsAmountDAI < 10, "taker gave wrong amount");

  // assert that neither offer posted by Amplifier are live (= have been retracted)
  Offer offer_on_dai = mgv.offers(olKeyWethDai, offerId1);
  Offer offer_on_usdc = mgv.offers(olKey, offerId2);
  assertTrue(!offer_on_dai.isLive(), "weth->dai offer should have been retracted");
  assertTrue(!offer_on_usdc.isLive(), "weth->usdc offer should have been retracted");
}

In verifying, we use a few helper functions provided by MangroveTest

  • cash(token, amount) - that helps us ensure that we use the correct decimals for a specific token,

  • minusFee(address_outbound, address_inbound, price) - that calculates the fee for a given market and price,

Amplifier.t.sol - Testing partial fill

function test_success_partialFill() public {
  deployStrat();

  execTraderStratWithPartialFillSuccess();
}
function execTraderStratWithPartialFillSuccess() public {
  uint makerGivesAmount = 0.15 ether;
  uint makerWantsAmountDAI = cash(dai, 300);
  uint makerWantsAmountUSDC = cash(usdc, 300);

  weth.approve($(strat.router()), type(uint).max);

  deal($(weth), $(this), cash(weth, 5));

  // post offers with Amplifier liquidity
  (uint offerId1, uint offerId2) =
    postAndFundOffers(makerGivesAmount, makerWantsAmountDAI, makerWantsAmountUSDC, GASREQ);

  //only take half of the offer
  (uint takerGot, uint takerGave,) = takeOffer(makerWantsAmountDAI / 2, dai, offerId1);

  // assert that
  uint offerGaveMinusFee = reader.minusFee(olKeyWethDai.flipped(), makerGivesAmount / 2);
  assertTrue(((takerGot - offerGaveMinusFee) * 10_000) / (makerGivesAmount / 2) < 10, "taker got wrong amount");
  assertEq(takerGave, makerWantsAmountDAI / 2, "taker gave wrong amount");

  // assert that neither offer posted by Amplifier are live (= have been retracted)
  Offer offer_on_dai = mgv.offers(olKeyWethDai, offerId1);
  Offer offer_on_usdc = mgv.offers(lo, offerId2);
  assertTrue(offer_on_dai.isLive(), "weth->dai offer should not have been retracted");
  assertTrue(offer_on_usdc.isLive(), "weth->usdc offer should not have been retracted");
}
https://github.com/mangrovedao/mangrove-strats/blob/a265abeb96a053e386d346c7c9e431878382749c/test/toy_strategies/Amplifier.t.sol#L143-L169

When you have sufficiently tested your %%maker contract|maker-contract%%, you may want to deploy your contract to a real chain.

Test Contract and Setup

In the implementation, we use standard cheatcodes, startPrank(<address>), stopPrank(<address> provided by Foundry for setting up the test (see ).

Testing a Successful Fill

We start by testing that we can take the offer fully without any rejections, and that after both offers are retracted (recall that was how we Amplifier). Let us call the test test_success_fill - in line with the naming guidelines of the .

Deploying Amplifier on the fork

After deployment, we use the helper provided by , which helps setup the correct token approvals for Mangrove for our .

Before calling activate() we also take the opportunity to demonstrate the related helper, , which checks whether necessary approvals have been setup, and, if not, reverts (recall that expectRevert(<message>) is a ).

Breaking down the test-specification further

post and fund a pair of offers using ,

Post and Fund Offers

Posting offers with Amplifier is simply a call to , sending along funds for the . (And we make a note to ensure in the test body that the testing contract has the funds for the .)

Taking a Single Offer

We make sure to impersonate the taker that we setup an address for . (Recall that Foundry provides for this exact purpose.)

And Finally, The Test

Approve the of Amplifier to access the WETH of the tester contract, which was given as when deploying the Amplifier (read more about ).

Provide the tester contract with some WETH to be able to successfully what the offer promises. (Using another cheatcode from Foundry, .)

Post and fund offers with .

Snipe one offer with .

Assert and verify the properties .

and a few view functions provided by Mangrove, offers() and isLive() (see ).

Running Tests with Foundry

In Foundry tests are run with the Forge tool, typically invoking something like forge test. Please refer to the for updated documentation.

Adding More Tests - Testing Partial Fills

With our helper functions for posting and sniping offers, we we can easily add more tests. For instance, testing that an offer was correctly handled for a looks like this:

Amplifier.t.sol - Helper function

A full test file for for the Amplifier contract can be found .

When you have sufficiently tested your , you may want to to a real chain.rovedao/mangrove-strats/blob/a265abeb96a053e386d346c7c9e431878382749c/test/toy_strategies/Amplifier.t.sol#L72-L76

A full test file for for the Amplifier contract can be found .

Post a Smart Offer
maker contract
MangroveTest
Foundry
Set Up Your Local Environment
Foundry -> Tests
Creating a Direct contract
​
​
MangroveTest
offer views
See entire file on GitHub
​
Foundry -> Cheatcodes
See entire file on GitHub
​
trade execution
decided to implement
Foundry test framework
See entire file on GitHub
​
activate()
MangroveOffer
maker contract
checkList()
Foundry cheatcode
See entire file on GitHub
​
Amplifier.newAmplifiedOffers(...)
​
newAmplifiedOffers
provision
provision
See entire file on GitHub
​
above
vm.prank()
See entire file on GitHub
​
router
reserveId
Approvals
give
deal()
postAndFundOffers()
takeOffer()
we listed above
See entire file on GitHub
Views on Offers
​
Foundry Book -> Tests
​
partial fill
See entire file on GitHub
See entire file on GitHub
here
maker contract
deploy your contract
here