Creating & Updating offers
How to write Mangrovian offers
Posting a new offer
New offers should mostly be posted by contracts able to source liquidity when asked by Mangrove.
function newOffer(
address outboundTkn,
address inboundTkn,
uint wants, // amount of inbound Tokens
uint gives, // amount of outbound Tokens
uint gasreq,
uint gasprice,
uint pivotId
) external payable returns (uint offerId);// logging new offer's data
event OfferWrite(
address outboundTkn,
address inboundTkn,
address maker, // account that created the offer, will be called upon execution
uint wants,
uint gives,
uint gasprice, // gasprice that was used to compute the offer bounty
uint gasreq,
uint offerId, // id of the new offer
uint prev // offer id of the closest best offer at the time of insertion
);
// `maker` balance on Mangrove (who is `msg.sender`) is debited of `amount` WEIs to provision the offer
event DebitWei(address maker, uint amount);
// `maker` balance on Mangrove is credited of `amount` WEIs if `msg.value > 0`.
event CreditWei(address maker, uint amount); // Gatekeeping
"mgv/dead" // Mangrove contract is terminated
"mgv/inactive" // Trying to post an offer in an inactive market
// Order book has reached its maximal number of orders (2**24)
"mgv/offerIdOverflow" // Unlikely as max offer id is 2**24
// Overflow
"mgv/writeOffer/gasprice/16bits"
"mgv/writeOffer/gives/96bits"
"mgv/writeOffer/wants/96bits"
// Invalid values
"mgv/writeOffer/gasreq/tooHigh" // gasreq above gasmax
"mgv/writeOffer/gives/tooLow" // gives should be > 0
"mgv/writeOffer/density/tooLow" // wants / (gasreq + overhead) < density
// Insufficient provision
"mgv/insufficientProvision" // provision of `msg.sender` should cover offer bountyimport "src/IMangrove.sol";
import {IERC20, MgvStructs} "src/MgvLib.sol";
// context of the call
address MGV;
address outTkn; // address offer's outbound token
address inbTkn; // address of offer's inbound token
address admin; // admin address of this contract
uint pivotId; // offer id whose price is the closest to this new offer (observed offchain)
// Approve Mangrove for outbound token transfer if not done already
IERC20(outTkn).approve(MGV, type(uint).max);
uint outDecimals = IERC20(outTkn).decimals();
uint inbDecimals = IERC20(inbTkn).decimals();
// importing global and local (pertaining to the (outTkn, inTkn) offer list) parameters.
(MgvStructs.GlobalPacked global, MgvStructs.LocalPacked local) = IMangrove(MGV)
.config(outTkn, inTkn);
uint gasprice = global.gasprice() * 10**9; // Mangrove's gasprice is in gwei units
uint gasbase = local.offer_gasbase() ; // gas necessary to process a market order
uint gasreq = 500_000; // assuming this logic requires 30K units of gas to execute
uint provision = (gasreq + gasbase) * gasprice; // minimal provision in wei
// calling mangrove with `pivotId` for initial positioning.
// sending `provision` amount of native tokens to cover for the bounty of the offer
IMangrove(MGV).newOffer{value: provision}(
outTkn, // reposting on the same market
inbTkn,
5.0*10**inbDecimals, // maker wants 5 inbound tokens
7.0*10**outDecimals, // maker gives 7 outbound tokens
30_000, // maker requires 500_000 gas units to comply
0, // use mangrove's gasprice oracle
pivotId // heuristic: tries to insert this offer after pivotId
); const {ethers} = require("ethers");
// context
// outTkn_address: address of outbound token ERC20
// inbTkn_address: address of inbound token ERC20
// ERC20_abi: ERC20 abi
// MGV_address: address of Mangrove
// MGV_abi: Mangrove contract's abi
// signer: ethers.js transaction signer
// loading ether.js contracts
const Mangrove = new ethers.Contract(
MGV_address,
MGV_abi,
ethers.provider
);
const InboundTkn = new ethers.Contract(
inbTkn_address,
ERC20_abi,
ethers.provider
);
const OutboundTkn = new ethers.Contract(
outTkn_address,
ERC20_abi,
ethers.provider
);
// if Mangrove is not approved yet for outbound token transfer.
await OutboundTkn.connect(signer).approve(MGV_address, ethers.constant.MaxUint256);
const outDecimals = await OutboundTkn.decimals();
const inbDecimals = await InboundTkn.decimals();
// putting takerGives/Wants in the correct format
const gives:ethers.BigNumber = ethers.parseUnits("8.0", outDecimals);
const wants:ethers.BigNumber = ethers.parseUnits("5.0", inbDecimals);
const {global, local} = await Mangrove.configInfo(outTkn_address,inbTkn_address);
// Market order at a limit average price of 8 outbound tokens given for 5 inbound tokens received
tx = await Mangrove.connect(signer).newOffer(
outTkn_address,
inbTkn_address,
wants,
gives,
0, // offer with no logic do not require additional gas to execute
global.gasprice, // using mangrove's gasprice
0, // using best offer as pivot
{value: (local.offer_gasbase.mul(global.gasprice).mul(10**9))} // putting funds on Mangrove to cover for offer bounty
);
await tx.wait();
Inputs
outbound_tknaddress of the outbound token (that the offer will provide).inbound_tknaddress of the inbound token (that the offer will receive).wantsamount of inbound tokens requested by the offer. Must fit in auint96.givesamount of outbound **** tokens promised by the offer. Must fit in auint96and be strictly positive. Must provide enough volume w.r.t togasreqand offer list's density parameter.gasreqamount of gas that will be given to the offer's account. Must fit in auint24and be lower than gasmax. Should be sufficient to cover all calls to the offer logic posting the offer (makerExecuteandmakerPosthook). Must be compatible with the offered volumegivesand the offer list's density parameter.gaspricegas price override used to compute the order provision (see offer bounties). Any value lower than Mangrove's current gasprice will be ignored (thus 0 means "use Mangrove's current gasprice"). Must fit in auint16.pivotIdwhere to start the insertion process in the offer list. IfpivotIdis not in the offer list at the time the transaction is processed, the new offer will be inserted starting from the offer list's best offer. Should be the id of the existing live offer with the price closest to the price of the offer being posted.
Outputs
offerIdthe id of the newly created offer. Note that offer ids are scoped to offer lists, so many offers can share the same id.
Provisioning
Since offers can fail, Mangrove requires each offer to be provisioned in ETH. If an offer fails, part of that provision will be sent to the caller that executed the offer, as compensation.
Make sure that your offer is well-provisioned before calling newOffer, otherwise the call will fail. The easiest way to go is to send a comfortable amount of ETH to Mangrove from your offer-posting contract. Mangrove will remember your ETH balance and use it when necessary.
Offer execution
If the offer account is a contract, it should implement the IMaker interface. At the very least, it must have a function with signature
makerExecute(MgvLib.SingleOrder calldata order)or it will systematically revert when called by Mangrove.givesandgasreqare subject to density constraints on the amount of outbound token provided per gas spent.The offer account will need to give Mangrove a high enough allowance in outbound tokens since Mangrove will use the ERC20 standard's
transferFromfunction to source your tokens.
Updating an existing offer
Offers are updated through the aptly-named updateOffer function described below (source code is here).
function updateOffer(
address outboundToken,
address inboundToken,
uint wants,
uint gives,
uint gasreq,
uint gasprice,
uint pivotId,
uint offerId
) external;event OfferWrite(
address outboundToken,
address inboundToken,
address maker, // account that created the offer, will be called upon execution
uint wants,
uint gives,
uint gasprice, // gasprice that was used to compute the offer bounty
uint gasreq,
uint offerId, // id of the updated offer
uint prev // offer id of the closest best offer at the time of update
);
// if old offer bounty is insufficient to cover the update,
// `maker` is debited of `amount` WEIs to complement the bounty
event DebitWei(address maker, uint amount);
// if old offer bounty is greater than the actual bounty,
// `maker` is credited of the corresponding `amount`.
event CreditWei(address maker, uint amount);
// Gatekeeping
"mgv/dead" // Mangrove contract is terminated
"mgv/inactive" // Trying to update an offer in an inactive market
// Type error in the arguments
"mgv/writeOffer/gasprice/16bits"
"mgv/writeOffer/gives/96bits"
"mgv/writeOffer/wants/96bits"
// Invalid values
"mgv/writeOffer/gasreq/tooHigh" // gasreq above gasmax
"mgv/writeOffer/gives/tooLow" // gives should be > 0
"mgv/writeOffer/density/tooLow" // wants / (gasreq + overhead) < density
// Invalid caller
"mgv/updateOffer/unauthorized" // caller must be the account that created the offer
// Insufficient provision
"mgv/insufficientProvision" // provision of caller no longer covers the offer bountyimport "src/IMangrove.sol";
import {MgvStructs} form "src/MgvLib.sol";
// context of the call
// MGV: address of Mangrove's deployment
// outTkn, inbTkn: addresses of the offer list in which the updated offer is
// offerId: offer identifier in the (outTkn, inbTkn) offer list
MgvStruct.OfferPacked memory offer32 = IMangrove(MGV).offers(outTkn, inbTkn, offerId);
MgvStruct.OfferPacked memory offerDetail32 = IMangrove(MGV).offerDetails(outTkn, inbTkn, offerId);
IMangrove(MGV).updateOffer(
outTkn,
inbTkn,
offer32.wants(), // do not update what the offer wants
offer32.gives() * 0.9, // decrease what offer gives by 10%
offerDetail32.gasreq(), // keep offer's current gasreq
offerDetail32.gasprice(), // keep offer's current gasprice
offer32.next(), // heuristic: use next offer as pivot since offerId might be off the book
offerId // id of the offer to be updated
);Inputs
offerIdis the offer id of the offer to be updated.For the other parameters, see above.
Outputs
None.
Reusing offers
After being executed or retracted, an offer is moved out of the offer list. It can still be updated and reinserted in the offer list. We recommend updating offers instead of creating new ones, as it costs much less gas.
Retracting an offer
An offer can be withdrawn from the order book via the retractOffer function described below.
function retractOffer(
address outboundToken,
address inboundToken,
uint offerId,
bool deprovision
) external;// emitted on all successful retractions
event OfferRetract(
address outboundToken, // address of the outbound token ERC of the offer
address inboundToken, // address of the inbound token ERC of the offer
uint offerId // the id of the offer that has been removed from the offer list
);
// emitted if offer is deprovisioned
event Credit(
address maker, // account being credited
uint amount // amount (in wei) being credited to the account
); "mgv/retractOffer/unauthorized" // only the offer's Maker Contract may call.import "./Mangrove.sol";
// context of the call
address MGV;
address outTkn; // address of market's base token
address inbTkn; // address of market's quote token
address admin; // admin address of this contract
...
...
// external function to update an offer
// assuming this contract has enough provision on Mangrove to repost the offer if need be
function myRetractOffer(uint offerId) external {
require(msg.sender == admin, "Invalid caller");
// calling mangrove with offerId as pivot (assuming price update will not change much the position of the offer)
Mangrove(MGV).retractOffer(
outTkn, // reposting on the same market
inbTkn,
offerId, // id of the offer to be updated
false // do not deprovision offer, saves gas if one wishes to repost the offer later
);
}
...
...Inputs
offerIdis the offer id of the offer to be updated.deprovisionif true, will free the offer's ETH provision so that you can withdraw them. Otherwise, will leave the provision in the offer.For the other parameters, see above.
Outputs
None.
Last updated