Post a Smart Offer
The tutorial assumes knowledge of solidity development. Follow preparation to create a new
Open your favorite solidity editor inside that folder.
It is assumed that the
ADMIN_ADDRESS has enough native tokens to complete the steps.
Simple maker contract (offer logic)
We want to create a new contract
OfferMakerTutorial to implement offer logic and utilize the
Direct contract in our strat-library for this purpose.
Direct provides a safety harness to make it easier to correctly interact with Mangrove, you can read more about it here.
Create a new
OfferMakerTutorial.sol file in the
src folder and add the following pieces.
Add the imports we are going to need, along with a standard solidity preamble.
Add the contract and the code for the constructor. We will skip some details here, which you can read more about later; routers, gas requirements, and deployment scripts. Note, we also implement the
ILiquidityProvider interface which makes the contract compatible with what the SDK expects.
Add offer posting function
The abstract contract
Direct has an internal function
_newOffer for posting offers on Mangrove. We need to expose this, so that we can post offers using our contract. We expose it through functions matching the
See OfferArgs for an explanation of the parameters for posting an offer.
To complete the
ILiquidityProvder interface we also need to provide an
updateOffer like so (details are not covered in this tutorial, but see Update Your Offer in the SDK for the convenience it provides):
Emit in Posthook
When using our new contract we can inspect traces and addresses, but for illustrative purposes insert the following to emit an event in the posthook when the offer is successfully taken.
The contract is now complete - you can see the full example by following the links to github.
Before deploying it on-chain we should test it.
First, compile the contract:
Start local node
Before proceeding, import the environment variables made as part of the preparation
Start Foundry's local node
anvil to test things locally before broadcasting to the real chain, with
$RPC_URL coming from
.env and pointing, for instance, to the Polygon Mumbai testnet.
anvil --fork-url $RPC_URL
Create contract on chain
Start another terminal and import environment variables again
Now, create the
OfferMakerTutorial contract on the
anvil node with your private key by pointing to its local
rpc-url, and supplying the parameters for Mangrove core contract (get it from Addresses for the network you have forked)
export MANGROVE=<contract address> # 0xabcd....
forge create "OfferMakerTutorial" --private-key "$PRIVATE_KEY" --rpc-url $LOCAL_URL --constructor-args "$MANGROVE" "$ADMIN_ADDRESS"
The output should look like
No files changed, compilation skipped
Deployed to: 0x5deA11A74e15BBfD6F29fd54F0b6F251F4774556
Transaction hash: 0xcfbe7503081ba9a749ee30fac8c40bfe19e0a5da665578ed00f40ce72694ca06
Take a note of the
Deployed to address and save it in a variable:
export OFFER_MAKER=<contract address> # 0xabcd..., the address of the newly deployed contract
Activate the contract
In this tutorial we will use the WETH, DAI market (the example token addresses are for the Polygon Mumbai testnet).
We have to let Mangrove pull the outbound token from our new contract, and we can use the
activate function for that.
cast send --rpc-url $LOCAL_URL "$OFFER_MAKER" "activate(address)" "[$WETH]" --private-key "$PRIVATE_KEY"
Post an offer
Now that the contract is ready, we can use it to post an offer - note that we have to provision the offer, and we do that by sending some native tokens to
cast send --rpc-url $LOCAL_URL "$OFFER_MAKER" "newOffer(address, address, uint, uint, uint)(uint)" "$WETH" "$DAI" 1000000000000000000 1700000000000000000000 0 --private-key "$PRIVATE_KEY" --value 0.01ether
Instead of trying to parse the logs, we can make a note of the
transactionHash at the end of the output and use local execution to see the
offerId returned by
cast run <transactionHash>
Which would output the following tail:
└─ ← 0x0000000000000000000000000000000000000000000000000000000000000235
Transaction successfully executed.
Gas used: 168639
0x0000000000000000000000000000000000000000000000000000000000000235 is the offer id.
Unlocked liquidity (reactive liquidity)
Notice that the offer was posted without transferring tokens from the admin to Mangrove or the
OfferMakerTutorial. This way the tokens are pulled just-in-time when the offer is taken and can thus be made available for other purposes.
For this to work, we need to let the
OfferMakerTutorial pull funds from the admin's reserve of WETH.
cast send --rpc-url $LOCAL_URL "$WETH" "approve(address, uint)" "$OFFER_MAKER" 1000000000000000000 --private-key "$PRIVATE_KEY"
Alternatively, the admin could transfer tokens to the contract and lock them until the offer is taken or the tokens are withdrawn.
OfferMakerTutorial uses the approval to transfer funds from the admin, but this could also involve a router and require additional approvals depending on the scenario. See approvals for more details.
A brief side step - if the admin (acting as a maker) does not have required WETH tokens then the smart offer will fail when taken (but note that it could still be posted - a smart offer can source liquidity on-chain).
If you don't have any WETH you can use this to mint some tokens:
cast send --rpc-url $LOCAL_URL "$WETH" "mint(uint)" 1000000000000000000000000000000 --private-key "$PRIVATE_KEY"
Shortly the admin will act as taker and take the offer, and therefore DAI are also needed.
cast send --rpc-url $LOCAL_URL "$DAI" "mint(uint)" 1000000000000000000000000000000 --private-key "$PRIVATE_KEY"
Snipe the offer
Before being able to take an offer we need to approve Mangrove to pull funds from the taker's DAI reserve:
cast send --rpc-url $LOCAL_URL "$DAI" "approve(address, uint)" "$MANGROVE" 1700000000000000000000 --private-key "$PRIVATE_KEY"
Now we ensure that we have set everything up correctly for the offer to be successfully taken. We use Mangrove's
snipes functionality to ensure it is exactly our own posted offer that we take, and not some other one in the order book. Takers would normally make market orders instead.
export OFFER_ID_HEX=<0xabcd...> # the hexadecimal offer ID captured when posting the offer
export OFFER_ID=$(($OFFER_ID_HEX)) # the decimal ID of the offer captured above
cast send --rpc-url $LOCAL_URL "$MANGROVE" "snipes(address, address, uint, bool)" "$WETH" "$DAI" "[[$OFFER_ID,1000000000000000000,1700000000000000000000,100000000000000000]]" 1 --private-key "$PRIVATE_KEY"
Now, run the resulting transaction via
cast run <transactionHash>
Towards the end of the trace you can find the function of
makerPosthook being called and emits an event with the data
42 as specified in the
__posthookSuccess__ of your new
├─  0xabc...::makerPosthook(...)
│ ├─ emit topic 0: 0xefg...
│ │ data: 0x000000000000000000000000000000000000000000000000000000000000002a
The next step could be to publish the contract on mainnet by stopping Anvil and replacing the
--rpc-url $LOCAL_URL in the above
approve commands with
--rpc-url $RPC_URL - and finally, the
newOffer with sensible prices.
To get a view of the order book, the Mangrove UI can be used, or you can use the SDK.
To get a better understanding of how tokens flow between taker, maker, Mangrove, and maker contracts like
OfferMakerTutorial, see Mangrove Offer.
If you get errors when taking the offer then it is probably missing approvals or (native) tokens.
mgv/takerTransferFail- missing DAI tokens or approval of transfer of DAI from taker to Mangrove.
makerExecuteis red in the trace - missing WETH tokens of approval of transfer of WETH from maker to Mangrove.