diff --git a/.github/getting-started.md b/.github/getting-started.md
deleted file mode 100644
index af2dab6..0000000
--- a/.github/getting-started.md
+++ /dev/null
@@ -1,204 +0,0 @@
-# Getting Started
-
-> [PoC Innovation's Open-Source project template](https://github.com/PoCInnovation/open-source-project-template)
-
-Please read carefully this guide.
-
-## Setup
-
-In this part, you will configure your project.
-
-### Branches
-
-Branch protection is really important. It helps you to have control on your code.
-
-For each of the following branches, add the required protections.
-
-#### `main`
-
-```markdown
-- [x] Require a pull request before merging
- - [x] Require approvals
- Required number of approvals before merging: 1
-
-- [x] Require status checks to pass before merging
- - [x] Require branches to be up to date before merging
-
-- [x] Require conversation resolution before merging
-```
-
-### Documents
-
-This template provides the must-have documents.
-
-#### README.md
-
-The README.md is the showcase of your project. It always must be clean and consistent. Otherwise, no one will care of your project.
-
-Fill every sections of the [README.md](/README.md).
-> If you add pictures, put them in the [assets](./assets/) folder.
-
-#### CONTRIBUTING.md
-
-The CONTRIBUTING.md is the guide to contribute to your project. It always must be clean and consistent. Otherwise, no one will contribute to your project.
-
-Fill every sections of the [CONTRIBUTING.md](/CONTRIBUTING.md).
-
-#### LICENSE
-
-The LICENSE protects your code and contributors.
-
-This template provides an [Apache Licence 2.0](https://www.apache.org/licenses/LICENSE-2.0).
-> If you want another one, check this [guide](https://choosealicense.com).
-
-If your project doesn't belong to [PoC Innovation](https://github.com/PoCInnovation), make sure to update the copyrights of the [LICENCE](/LICENSE).
-
-### About
-
-Update the `About` section by adding a description, a website, and topics.
-
-### Templates
-
-This template provides the must-have templates.
-
-#### Issues
-
-An issue is a tool to track and focus tasks.
-
-This template provides two issues templates :
-- `Bug Report`
-- `Feature Request`
-
-Change the default assignee of the [bug_report](./ISSUE_TEMPLATE/bug_report.yml) template.
-
-Change the default assignee of the [feature_request](./ISSUE_TEMPLATE/feature_request.yml) template.
-
-#### Pull Requests
-
-A pull request is an event where a contributor asks a maintainer to review code.
-
-This template provides a [pull request template](./pull_request_template.md). You don't need to update it.
-
-#### Milestones
-
-A milestone helps to track progress on groups of issues or pull requests.
-
-This template provides a [milestone template](./milestone_template.md). You don't need to update it.
-
-### Labels
-
-A label helps to categorize issues and pull requests.
-
-Make sure to have the following labels :
-
-- `bug`: Something isn't working
-- `bugfix`: Resolve a bug
-- `chore`: Global maintenance
-- `documentation`: Improvements or additions to documentation
-- `duplicate`: This issue or pull request already exists
-- `enhancement`: New feature or request
-- `help wanted`: Extra attention is needed
-- `invalid`: This doesn't seem right
-- `major`: Major update (for release)
-- `minor`: Minor update (for release)
-- `patch`: Patch update (for release)
-- `question`: Further information is requested
-- `triage`: Need to be tagged
-- `wontfix`: This will not be worked on
-
-### GitHub project
-
-Create a GitHub project to manage your milestones, issues and pull requests.
-
-### Actions
-
-This template provides some GitHub actions.
-
-#### Release Drafter
-
-A release is tool with changelogs that present a full history of a project.
-
-This template provides an [action](./workflows/release-drafter.yml) that drafts [next releases notes](./release-drafter.yml) as pull requests are merged into the main branch. You don't need to update it.
-> Check this [action's documentation](https://github.com/release-drafter/release-drafter) to understand how it works
-
-### Settings
-
-#### Visibility
-
-Make your repository public.
-
-## Sprints
-
-In this part, you will learn how to manage sprints.
-
-A sprint is associated as a milestone.\
-A task is associated as an issue.
-
-### Workflow
-
-The workflow to follow is:
-
-1) Create a milestone
-2) Create all the needed issues linked to this milestone
-3) Manage the pull requests linked with these issues using the GitHib project
-4) Resolve these issues
-5) Publish a release
-6) Close the milestone
-
-### Milestones
-
-Each milestones must use the [milestone template](./milestone_template.md).
-
-There are two parts :
-- Overall
- > **⚠️ It's checklist must be completed before starting this sprint ⚠️**
-- Final Report
- > **⚠️ It's checklist must be completed before starting a new sprint ⚠️**
-
-Additional information is written in the milestones's checklists. Read them carefully!
-
-### Issues
-
-Create all the required issues of a sprint before starting it. Once the sprint started, no issue linked to it should be create.
-
-**Each issue must be linked to a milestone and a GitHub project, have the right labels and be assigned to someone.**
-
-You can discuss in a issue, do it as much as you can!
-
-### Pull Requests
-
-**Each pull request must be linked to an issue and a GitHub project, have the right labels, be assigned to someone and have a reviewer.**
-
-You can discuss in a pull request, do it as much as you can!
-
-### GitHub project
-
-**No tasks (issue) must be created directly from the GitHub project. Create an issue using a template, it will automatically appears on the GitHub project. Don't forget to archive the tasks once the milestone is closed.**
-
-It is a powerful tool, use it well!
-
-### Releases
-
-**Each update on the main branch must be linked to a release.**
-
-Tag pull requests with the `patch`, `minor` or `major` labels.
-
-## Notes
-
-### Discord Webhook
-
-We strongly advice you to have a discord channel on which you will receive GitHub updates on your project.
-
-Follow this [tutorial](https://gist.github.com/SGTGunner/50d6a3cc0d489cf779f77695ba3e22ea).
-
-### Security dependabot
-
-We strongly advice you to have a security dependabot to fix vulnerable dependencies.
-
-Follow this [tutorial](https://docs.github.com/en/code-security/dependabot/dependabot-security-updates/configuring-dependabot-security-updates#managing-dependabot-security-updates-for-your-repositories).
-
-### Help
-
-If you have any questions, please contact [Reza Rahemtola](https://github.com/RezaRahemtola).
-
-> Made with ❤️ by PoC
diff --git a/.github/milestone_template.md b/.github/milestone_template.md
deleted file mode 100644
index 69090de..0000000
--- a/.github/milestone_template.md
+++ /dev/null
@@ -1,26 +0,0 @@
-## Overall
-
-### Objective
-
-[Explain here the objective of the milestone]
-
-### Checklist
-
-- [ ] Clear objective
-- [ ] Consistent objective
-- [ ] Achievable in the given time
-- [ ] Issues created, with the rights labels and linked to this milestone
-- [ ] Issues assigned
-
-## Final Report
-
-### Checklist
-
-- [ ] Objective fulfilled
-- [ ] README.md and other relevant documents (guide, ...) updated
-- [ ] Documentation updated
-- [ ] Pull requests merged
-- [ ] Issues closed
-- [ ] Release created
-- [ ] Tasks archived
-- [ ] Branches cleared
diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml
deleted file mode 100644
index d012e25..0000000
--- a/.github/workflows/release-drafter.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-name: Release Drafter
-
-on:
- push:
- # branches to consider in the event; optional, defaults to all
- branches:
- - main
- # pull_request event is required only for autolabeler
- pull_request:
- # Only following types are handled by the action, but one can default to all as well
- types: [opened, reopened, synchronize]
- # pull_request_target event is required for autolabeler to support PRs from forks
- # pull_request_target:
- # types: [opened, reopened, synchronize]
-
-jobs:
- update_release_draft:
- runs-on: ubuntu-latest
- steps:
- # Drafts your next Release notes as Pull Requests are merged into "main"
- - uses: release-drafter/release-drafter@v5
- # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
- # with:
- # config-name: my-config.yml
- # disable-autolabeler: true
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/tests-runner.yml b/.github/workflows/tests-runner.yml
new file mode 100644
index 0000000..b12b953
--- /dev/null
+++ b/.github/workflows/tests-runner.yml
@@ -0,0 +1,30 @@
+on:
+ pull_request:
+ branches:
+ - main
+ push:
+ branches:
+ - main
+
+name: Test runner
+
+jobs:
+ check:
+ name: Foundry project
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ submodules: recursive
+
+ - name: Install Foundry
+ uses: foundry-rs/foundry-toolchain@v1
+
+ - name: Install packages
+ run: npm install
+
+ - name: Run tests
+ run: forge test -vvvvv
+
+ - name: Run snapshot
+ run: forge snapshot
diff --git a/README.md b/README.md
index 49bf341..53df577 100644
--- a/README.md
+++ b/README.md
@@ -41,8 +41,8 @@ If you're interested in how the project is organized at a higher level, please c
## Our PoC team ❤️
Developers
-| [
Martin Saldinger](https://github.com/LeTamanoir) | [
Florian Lauch](https://github.com/EdenComp) | [
Nathan Flattin](https://github.com/Nfire2103) |
-| :--------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------: |
+| [
Martin Saldinger](https://github.com/LeTamanoir) | [
Nathan Flattin](https://github.com/Nfire2103) |
+| :--------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------: |
Manager
| [
Ismaël Fall](https://github.com/Doozers) |
diff --git a/package.json b/package.json
index 7576b2e..444f9cd 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
"url": "git+https://github.com/PoCInnovation/Price-Sensor.git"
},
"keywords": [],
- "author": "Martin Saldinger, Nathan Flattin, Florian Lauch",
+ "author": "Martin Saldinger, Nathan Flattin",
"license": "ISC",
"bugs": {
"url": "https://github.com/PoCInnovation/Price-Sensor/issues"
diff --git a/remappings.txt b/remappings.txt
index beeca25..2dba6a2 100644
--- a/remappings.txt
+++ b/remappings.txt
@@ -3,7 +3,4 @@ mgv_lib/=node_modules/@mangrovedao/mangrove-core/lib/
mgv_test/=node_modules/@mangrovedao/mangrove-core/test/
mgv_script/=node_modules/@mangrovedao/mangrove-core/script/
-ds-test/=node_modules/@mangrovedao/mangrove-core/lib/forge-std/lib/ds-test/src/
forge-std/=node_modules/@mangrovedao/mangrove-core/lib/forge-std/src/
-
-uniswap/=node_modules/@uniswap/v3-core/contracts/
diff --git a/script/OfferMakerTutorialScript.sol b/script/OfferMakerTutorialScript.sol
deleted file mode 100644
index 366d929..0000000
--- a/script/OfferMakerTutorialScript.sol
+++ /dev/null
@@ -1,129 +0,0 @@
-// SPDX-License-Identifier: Unlicense
-pragma solidity ^0.8.10;
-
-import "forge-std/Script.sol";
-import {OfferMakerTutorial} from "../src/OfferMakerTutorial.sol";
-import {IMangrove} from "mgv_src/IMangrove.sol";
-import {IERC20} from "mgv_src/IERC20.sol";
-import {MgvLib} from "mgv_src/MgvLib.sol";
-import {AbstractRouter} from "mgv_src/strategies/routers/AbstractRouter.sol";
-
-contract Base is Script {
- uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
- address deployerAddress = vm.envAddress("ADMIN_ADDRESS");
-
- address MGV = vm.envAddress("MANGROVE");
- address USDT = vm.envAddress("USDT");
- address WMATIC = vm.envAddress("WMATIC");
-}
-
-contract DeployOfferMaker is Base {
- function run() public {
- vm.startBroadcast(deployerPrivateKey);
-
- OfferMakerTutorial offerMaker = new OfferMakerTutorial(IMangrove(payable(MGV)), deployerAddress);
-
- IERC20[] memory tokens = new IERC20[](2);
- tokens[0] = IERC20(USDT);
- tokens[1] = IERC20(WMATIC);
-
- offerMaker.activate(tokens);
-
- console.log("offerMakerTutorial deployed at: ", address(offerMaker));
-
- vm.stopBroadcast();
- }
-}
-
-contract MintTokens is Base {
- address OFFER_MAKER = vm.envAddress("OFFER_MAKER");
-
- function mintToken(address token, uint256 amount) private {
- (bool send,) = token.call(abi.encodeWithSignature("mint(uint256)", amount));
- require(send, "mint failed");
- }
-
- function run() public {
- vm.startBroadcast(deployerPrivateKey);
-
- uint256 mintAmount = 100000000000;
- mintToken(USDT, mintAmount);
- mintToken(WMATIC, mintAmount);
-
- address router = address(OfferMakerTutorial(payable(OFFER_MAKER)).router());
-
- IERC20(USDT).approve(router, IERC20(USDT).balanceOf(deployerAddress));
- IERC20(WMATIC).approve(router, IERC20(WMATIC).balanceOf(deployerAddress));
-
- IERC20(USDT).approve(OFFER_MAKER, IERC20(USDT).balanceOf(deployerAddress));
- IERC20(WMATIC).approve(OFFER_MAKER, IERC20(WMATIC).balanceOf(deployerAddress));
-
- IERC20(USDT).approve(MGV, IERC20(USDT).balanceOf(deployerAddress));
- IERC20(WMATIC).approve(MGV, IERC20(WMATIC).balanceOf(deployerAddress));
-
- vm.stopBroadcast();
- }
-}
-
-contract TokensBalance is Base {
- address OFFER_MAKER = vm.envAddress("OFFER_MAKER");
-
- function run() public {
- vm.startBroadcast(deployerPrivateKey);
-
- address router = address(OfferMakerTutorial(payable(OFFER_MAKER)).router());
-
- console.log("USDT balance: \t\t", IERC20(USDT).balanceOf(deployerAddress));
- console.log("WMATIC balance: \t\t", IERC20(WMATIC).balanceOf(deployerAddress));
-
- console.log("USDT allowance (MAKER): \t", IERC20(USDT).allowance(deployerAddress, OFFER_MAKER));
- console.log("WMATIC allowance (MAKER): \t", IERC20(WMATIC).allowance(deployerAddress, OFFER_MAKER));
-
- console.log("USDT allowance (MGV): \t", IERC20(USDT).allowance(deployerAddress, MGV));
- console.log("WMATIC allowance (MGV): \t", IERC20(WMATIC).allowance(deployerAddress, MGV));
-
- console.log("USDT allowance (router): \t", IERC20(USDT).allowance(deployerAddress, router));
- console.log("WMATIC allowance (router): \t", IERC20(WMATIC).allowance(deployerAddress, router));
-
- vm.stopBroadcast();
- }
-}
-
-contract PostOffer is Base {
- address OFFER_MAKER = vm.envAddress("OFFER_MAKER");
-
- function run() public {
- vm.startBroadcast(deployerPrivateKey);
-
- uint256 wants = 10000000000;
- uint256 gives = 1000000000;
- uint256 gasreq = 1_000_000;
-
- uint256 offerId = OfferMakerTutorial(payable(OFFER_MAKER)).newOffer{value: 0.01 ether}(
- IERC20(USDT), IERC20(WMATIC), wants, gives, 0, gasreq
- );
-
- console.log("offerId: ", offerId);
-
- vm.stopBroadcast();
- }
-}
-
-contract SnipeOffer is Base {
- uint256 OFFER_ID = vm.envUint("OFFER_ID");
-
- function run() public {
- vm.startBroadcast(deployerPrivateKey);
-
- uint256[4][] memory targets = new uint [4][](1);
-
- targets[0][0] = OFFER_ID;
- targets[0][1] = 10000000;
- targets[0][2] = 100000000;
- targets[0][3] = 1000000 + 10;
-
- IMangrove(payable(MGV)).snipes(USDT, WMATIC, targets, true);
-
- vm.stopBroadcast();
- }
-}
diff --git a/src/AbstractPriceSensor.sol b/src/AbstractPriceSensor.sol
new file mode 100644
index 0000000..58e3d52
--- /dev/null
+++ b/src/AbstractPriceSensor.sol
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: Unlicense
+pragma solidity ^0.8.10;
+
+import {IMangrove} from "mgv_src/IMangrove.sol";
+import {MgvLib, MgvStructs} from "mgv_src/MgvLib.sol";
+import {IERC20} from "mgv_src/IERC20.sol";
+
+import {IPriceSensor} from "./IPriceSensor.sol";
+
+/// @title PriceSensor
+/// @author Martin Saldinger, Nathan Flattin
+/// @dev This contract is used to create sensors that trigger a stop loss when a certain price is reached
+/// @dev You need to inherit from this contract and implement the `__callbackOnStopLoss__` function
+abstract contract AbstractPriceSensor is IPriceSensor {
+ /// The mangrove contract
+ IMangrove private immutable _MGV;
+
+ /// @param mgv_ The mangrove contract
+ constructor(address mgv_) {
+ _MGV = IMangrove(payable(mgv_));
+ }
+
+ /// @notice callback function used to check if an offer was taken without sniping and if the stop loss was reached
+ /// @dev make sure to call this function when an offer is taken using for example `__posthookSuccess__`, check the example folder for an example implementation
+ /// @param order the order that was taken
+ function __callbackOnOfferTaken__(
+ MgvLib.SingleOrder calldata order
+ ) internal {
+ uint256 newBestOfferId = _MGV.best(
+ order.outbound_tkn,
+ order.inbound_tkn
+ );
+
+ (MgvStructs.OfferUnpacked memory newBestOffer, ) = _MGV.offerInfo(
+ order.outbound_tkn,
+ order.inbound_tkn,
+ newBestOfferId
+ );
+
+ // newBestOffer.wants order.offer.wants()
+ // newBestPrice = ------------------ >= oldPrice = -------------------
+ // newBestOffer.gives order.offer.gives()
+ //
+ // if newBestPrice is lower than oldPrice, then the stop loss was reached
+
+ if (
+ order.offer.gives() * newBestOffer.wants >=
+ newBestOffer.gives * order.offer.wants()
+ ) {
+ __callbackOnStopLoss__(order);
+ }
+ // else repost the offer
+ else {
+ _MGV.updateOffer(
+ order.outbound_tkn,
+ order.inbound_tkn,
+ order.offer.wants(),
+ order.offer.gives(),
+ order.offerDetail.gasreq(),
+ order.offerDetail.gasprice(),
+ order.offer.next(),
+ order.offerId
+ );
+ }
+ }
+
+ /// @notice Callback function called when a stop loss is reached and no snipe is detected
+ /// @param order the bait offer id
+ function __callbackOnStopLoss__(
+ MgvLib.SingleOrder calldata order
+ ) internal virtual {
+ emit StopLossReached(order.offerId);
+ }
+}
diff --git a/src/IPriceSensor.sol b/src/IPriceSensor.sol
new file mode 100644
index 0000000..425869c
--- /dev/null
+++ b/src/IPriceSensor.sol
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: Unlicense
+pragma solidity ^0.8.10;
+
+interface IPriceSensor {
+ /// @param baitOfferId The index of the newly created sensor
+ event NewSensor(uint256 indexed baitOfferId);
+
+ /// @param baitOfferId The index of the sensor where the stop loss was reached
+ event StopLossReached(uint256 indexed baitOfferId);
+
+ /// @notice Event emitted when a stop loss is reached
+ /// @param outboundToken The outbound token of the sensor
+ /// @param inboundToken The inbound token of the sensor
+ event StopLossReached(
+ address indexed outboundToken,
+ address indexed inboundToken
+ );
+
+ event DEBUG(uint256 p1, uint256 p2);
+}
diff --git a/src/OfferMakerTutorial.sol b/src/OfferMakerTutorial.sol
deleted file mode 100644
index 72783fa..0000000
--- a/src/OfferMakerTutorial.sol
+++ /dev/null
@@ -1,101 +0,0 @@
-// SPDX-License-Identifier: Unlicense
-pragma solidity ^0.8.10;
-
-// Import the types we will be using below
-import {Direct} from "mgv_src/strategies/offer_maker/abstract/Direct.sol";
-import {ILiquidityProvider} from "mgv_src/strategies/interfaces/ILiquidityProvider.sol";
-import {IMangrove} from "mgv_src/IMangrove.sol";
-import {IERC20, MgvLib} from "mgv_src/MgvLib.sol";
-import {SimpleRouter} from "mgv_src/strategies/routers/SimpleRouter.sol";
-
-/// @title An example offer maker used in tutorials
-contract OfferMakerTutorial is Direct, ILiquidityProvider {
- ///@notice Constructor
- ///@param mgv The core Mangrove contract
- ///@param deployer The address of the deployer
- constructor(
- IMangrove mgv,
- address deployer
- ) Direct(mgv, new SimpleRouter(), 100_000, deployer) {
- router().bind(address(this));
- }
-
- ///@inheritdoc ILiquidityProvider
- function newOffer(
- IERC20 outbound_tkn,
- IERC20 inbound_tkn,
- uint256 wants,
- uint256 gives,
- uint256 pivotId,
- uint256 gasreq /* the function is payable to allow us to provision an offer*/
- )
- public
- payable
- onlyAdmin /* only the admin of this contract is allowed to post offers using this contract*/
- returns (uint256 offerId)
- {
- (offerId, ) = _newOffer(
- OfferArgs({
- outbound_tkn: outbound_tkn,
- inbound_tkn: inbound_tkn,
- wants: wants,
- gives: gives,
- gasreq: gasreq,
- gasprice: 0,
- pivotId: pivotId, // a best pivot estimate for cheap offer insertion in the offer list - this should be a parameter computed off-chain for cheaper insertion
- fund: msg.value, // WEIs in that are used to provision the offer.
- noRevert: false // we want to revert on error
- })
- );
- }
-
- ///@inheritdoc ILiquidityProvider
- function updateOffer(
- IERC20 outbound_tkn,
- IERC20 inbound_tkn,
- uint256 wants,
- uint256 gives,
- uint256 pivotId,
- uint256 offerId,
- uint256 gasreq
- ) public payable override adminOrCaller(address(MGV)) {
- _updateOffer(
- OfferArgs({
- outbound_tkn: outbound_tkn,
- inbound_tkn: inbound_tkn,
- wants: wants,
- gives: gives,
- gasreq: gasreq,
- gasprice: 0,
- pivotId: pivotId,
- fund: msg.value,
- noRevert: false
- }),
- offerId
- );
- }
-
- ///@inheritdoc ILiquidityProvider
- function retractOffer(
- IERC20 outbound_tkn,
- IERC20 inbound_tkn,
- uint256 offerId,
- bool deprovision
- ) public adminOrCaller(address(MGV)) returns (uint256 freeWei) {
- return _retractOffer(outbound_tkn, inbound_tkn, offerId, deprovision);
- }
-
- ///@notice Event emitted when the offer is taken successfully.
- ///@param someData is a dummy parameter.
- event OfferTakenSuccessfully(uint256 someData);
-
- ///@notice Post-hook that is invoked when the offer is taken successfully.
- ///@inheritdoc Direct
- function __posthookSuccess__(
- MgvLib.SingleOrder calldata,
- bytes32
- ) internal virtual override returns (bytes32) {
- emit OfferTakenSuccessfully(42);
- return 0;
- }
-}
diff --git a/src/abstract/PriceSensor.sol b/src/abstract/PriceSensor.sol
deleted file mode 100644
index 3877c8d..0000000
--- a/src/abstract/PriceSensor.sol
+++ /dev/null
@@ -1,191 +0,0 @@
-// SPDX-License-Identifier: Unlicense
-pragma solidity ^0.8.10;
-
-import {IMangrove} from "mgv_src/IMangrove.sol";
-import {IUniswapV3Pool} from "uniswap/interfaces/IUniswapV3Pool.sol";
-import {MgvLib, MgvStructs} from "mgv_src/MgvLib.sol";
-import {SetLib, Set} from "../library/Set.sol";
-
-interface IPriceSensor {
- /// @param baitOfferId The index of the newly created sensor
- event NewSensor(uint256 indexed baitOfferId);
-
- /// @param baitOfferId The index of the sensor where the stop loss was reached
- event StopLossReached(uint256 indexed baitOfferId);
-
- /// @notice Event emitted when a stop loss is reached
- /// @param outboundToken The outbound token of the sensor
- /// @param inboundToken The inbound token of the sensor
- /// @param price The price of the sensor on which the stop loss should be triggered
- event StopLossReached(
- address indexed outboundToken,
- address indexed inboundToken,
- uint256 price
- );
-
- /// @notice Error emitted when the address is 0
- error ZeroAddressNotAllowed();
-
- /// @notice Error emitted when the price is 0
- error ZeroPriceNotAllowed();
-}
-
-/// @title PriceSensor
-/// @author Martin Saldinger, Florian Lauch, Nathan Flattin
-/// @dev This contract is used to create sensors that trigger a stop loss when a certain price is reached
-/// @dev You need to inherit from this contract and implement the `__callbackOnStopLoss__` function
-abstract contract PriceSensor is IPriceSensor {
- using SetLib for Set;
-
- /// mapping of watched uniwap pools for outbound and inbound token
- mapping(address outboundToken => mapping(address inboundToken => Set))
- private _uniswapPools;
-
- /// The mangrove contract
- IMangrove private immutable _MGV;
-
- /// @param mgv_ The mangrove contract
- constructor(address mgv_) {
- _MGV = IMangrove(payable(mgv_));
- }
-
- /// @notice Creates a new sensor
- /// @param outboundToken the outbound token of the sensor
- /// @param inboundToken the inbound token of the sensor
- /// @param price the price of the sensor on which the stop loss should be triggered
- /// @param gasreq the gas requirement of the sensor (mostly by the __callbackOnStopLoss__ function)
- /// @param pivotId index in order book (the more precise the pivot, the less expensive it is in gas)
- /// @return offerId the mangrove offerId of the newly bait offer
- function _newSensor(
- address[] calldata uniswapPools,
- address outboundToken,
- address inboundToken,
- uint256 price,
- uint256 gasreq,
- uint256 pivotId
- ) internal returns (uint256 offerId) {
- if (outboundToken == address(0) || inboundToken == address(0)) {
- revert ZeroAddressNotAllowed();
- }
- if (price == 0) {
- revert ZeroPriceNotAllowed();
- }
-
- (, MgvStructs.LocalUnpacked memory local) = _MGV.configInfo(
- outboundToken,
- inboundToken
- );
-
- /// bypass mangrove check of minimum gives
- uint256 gives = (gasreq + local.offer_gasbase) * local.density;
- uint256 wants = price / (1 ether / gives);
-
- offerId = _MGV.newOffer{value: msg.value}(
- outboundToken,
- inboundToken,
- wants,
- gives,
- gasreq,
- 0,
- pivotId
- );
-
- // keep the uniswap pools sorted
- if (outboundToken > inboundToken) {
- (outboundToken, inboundToken) = (inboundToken, outboundToken);
- }
-
- /// add the uniswap pools to the mapping
- for (uint256 i = 0; i < uniswapPools.length; ) {
- _uniswapPools[outboundToken][inboundToken].add(uniswapPools[i]);
-
- unchecked {
- ++i;
- }
- }
-
- emit NewSensor(offerId);
- }
-
- /// @notice Removes a sensor
- /// @param outboundToken outbound token of the sensor
- /// @param inboundToken inbound token of the sensor
- /// @param id the id of the sensor to remove
- function _removeSensor(
- address outboundToken,
- address inboundToken,
- uint256 id
- ) internal returns (uint256 provision) {
- /// retract Mangrove offer
- provision = _MGV.retractOffer(outboundToken, inboundToken, id, true);
- }
-
- /// @notice callback function used to check if an offer was taken without sniping and if the stop loss was reached
- /// @dev make sure to call this function when an offer is taken using for example `__posthookSuccess__`, check the example folder for an example implementation
- /// @param order the order that was taken
- function __callbackOnOfferTaken__(
- MgvLib.SingleOrder calldata order
- ) internal {
- /// reconstruct the price
- uint256 price = order.offer.wants() * (1 ether / order.offer.gives());
-
- address outboundToken = order.outbound_tkn;
- address inboundToken = order.inbound_tkn;
-
- if (outboundToken > inboundToken) {
- (outboundToken, inboundToken) = (inboundToken, outboundToken);
- }
-
- Set storage uniswapPools = _uniswapPools[outboundToken][inboundToken];
-
- // don't do anything if there are no uniswap pools
- // for safety sensors without uniswap pools will never be reposted
- if (uniswapPools.values.length == 0) {
- return;
- }
-
- uint256 average = 0;
-
- for (uint256 i = 0; i < uniswapPools.values.length; ) {
- (uint160 sqrtPriceX96, , , , , , ) = IUniswapV3Pool(
- uniswapPools.values[i]
- ).slot0();
-
- /// compute the pool price
- /// https://blog.uniswap.org/uniswap-v3-math-primer
- uint256 poolPrice = (sqrtPriceX96 / 2 ** 96) ** 2;
-
- unchecked {
- average += poolPrice;
- ++i;
- }
- }
-
- average /= uniswapPools.values.length;
-
- // if the average price is lower than the price of the sensor it means that the stop loss was reached
- if (average > price) {
- __callbackOnStopLoss__(order);
- } else {
- // else we repost the offer
- _MGV.updateOffer(
- order.outbound_tkn,
- order.inbound_tkn,
- order.wants,
- order.gives,
- order.offerDetail.gasreq(),
- order.offerDetail.gasprice(),
- order.offer.next(),
- order.offerId
- );
- }
- }
-
- /// @notice Callback function called when a stop loss is reached and no snipe is detected
- /// @param order the bait offer id
- function __callbackOnStopLoss__(
- MgvLib.SingleOrder calldata order
- ) internal virtual {
- emit StopLossReached(order.offerId);
- }
-}
diff --git a/src/example/ExampleImplementation.sol b/src/example/ExampleImplementation.sol
index 647c30b..371ce3f 100644
--- a/src/example/ExampleImplementation.sol
+++ b/src/example/ExampleImplementation.sol
@@ -5,51 +5,61 @@ import {IMangrove} from "mgv_src/IMangrove.sol";
import {MgvLib} from "mgv_src/MgvLib.sol";
import {Direct} from "mgv_src/strategies/offer_maker/abstract/Direct.sol";
import {SimpleRouter} from "mgv_src/strategies/routers/SimpleRouter.sol";
+import {IERC20} from "mgv_src/IERC20.sol";
-import {PriceSensor} from "../abstract/PriceSensor.sol";
+import {AbstractPriceSensor} from "../AbstractPriceSensor.sol";
-contract ExampleImplementation is PriceSensor, Direct {
- event TestEvent(uint256 indexed offerId);
+contract ExampleImplementation is AbstractPriceSensor, Direct {
+ /// The outbound token of the sensor
+ IERC20 private immutable _outbound_tkn;
+ /// The inbound token of the sensor
+ IERC20 private immutable _inbound_tkn;
+
+ /// The gas requirement of the sensor (mostly used for the __callbackOnStopLoss__ function)
+ uint256 private immutable _gasreq;
constructor(
address mgv,
- address deployer
+ address deployer,
+ address outbound_token_,
+ address inbound_token_,
+ uint256 gasreq_
)
Direct(IMangrove(payable(mgv)), new SimpleRouter(), 100_000, deployer)
- PriceSensor(mgv)
+ AbstractPriceSensor(mgv)
{
router().bind(address(this));
+ _outbound_tkn = IERC20(outbound_token_);
+ _inbound_tkn = IERC20(inbound_token_);
+ _gasreq = gasreq_;
}
function newSensor(
- address[] calldata uniswapPools,
- address outboundToken,
- address inboundToken,
- uint256 price
+ uint256 wants,
+ uint256 gives,
+ uint256 gasprice,
+ uint256 pivotId
) public payable returns (uint256 offerId) {
- offerId = _newSensor(
- uniswapPools,
- outboundToken,
- inboundToken,
- price,
- 30_000,
- 0
+ (offerId, ) = _newOffer(
+ OfferArgs({
+ outbound_tkn: _outbound_tkn,
+ inbound_tkn: _inbound_tkn,
+ wants: wants,
+ gives: gives,
+ gasreq: _gasreq,
+ gasprice: gasprice,
+ pivotId: pivotId,
+ fund: msg.value,
+ noRevert: false // useful for testing
+ })
);
}
- function removeSensor(
- address outboundToken,
- address inboundToken,
- uint256 id
- ) public returns (uint256 provision) {
- provision = _removeSensor(outboundToken, inboundToken, id);
- }
-
function __posthookSuccess__(
MgvLib.SingleOrder calldata order,
bytes32 makerData
) internal override returns (bytes32) {
- super.__callbackOnOfferTaken__(order);
+ __callbackOnOfferTaken__(order);
return super.__posthookSuccess__(order, makerData);
}
@@ -57,8 +67,9 @@ contract ExampleImplementation is PriceSensor, Direct {
MgvLib.SingleOrder calldata order
) internal virtual override {
// Do something with the sensor data
- emit TestEvent(order.offerId);
- // Call the default parent function (for logging)
+ // e.g. sell all the inbound token for the outbound token
+
+ // Call the default parent function if you need logging (e.g. for an oracle etc...)
super.__callbackOnStopLoss__(order);
}
}
diff --git a/src/library/Set.sol b/src/library/Set.sol
deleted file mode 100644
index 7e935f7..0000000
--- a/src/library/Set.sol
+++ /dev/null
@@ -1,16 +0,0 @@
-// SPDX-License-Identifier: Unlicense
-pragma solidity ^0.8.10;
-
-struct Set {
- address[] values;
- mapping(address => bool) has;
-}
-
-library SetLib {
- function add(Set storage set, address value) public {
- if (set.has[value] == false) {
- set.values.push(value);
- set.has[value] = true;
- }
- }
-}
diff --git a/test/PriceSensor.t.sol b/test/PriceSensor.t.sol
index c91050e..7ad4b48 100644
--- a/test/PriceSensor.t.sol
+++ b/test/PriceSensor.t.sol
@@ -1,22 +1,25 @@
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.10;
-import {Test2} from "mgv_lib/Test2.sol";
-import {Test, console} from "forge-std/Test.sol";
+import "forge-std/Test.sol";
+import {Mangrove} from "mgv_src/Mangrove.sol";
import {IMangrove} from "mgv_src/IMangrove.sol";
import {MgvStructs} from "mgv_src/MgvLib.sol";
import {IERC20} from "mgv_src/IERC20.sol";
-import {PriceSensor} from "../src/abstract/PriceSensor.sol";
-import {ExampleImplementation} from "../src/example/ExampleImplementation.sol";
-
-import {AbstractMangrove} from "mgv_src/AbstractMangrove.sol";
-import {Mangrove} from "mgv_src/Mangrove.sol";
import {TransferLib} from "mgv_src/strategies/utils/TransferLib.sol";
-import {TestToken} from "mgv_test/lib/tokens/TestToken.sol";
+import {AbstractMangrove} from "mgv_src/AbstractMangrove.sol";
import {AbstractRouter} from "mgv_src/strategies/routers/AbstractRouter.sol";
+import {IOfferLogic} from "mgv_src/strategies/interfaces/IOfferLogic.sol";
-contract MangroveTestHelper {
+import {TestToken} from "mgv_test/lib/tokens/TestToken.sol";
+
+import {Test2} from "mgv_lib/Test2.sol";
+
+import {IPriceSensor} from "../src/IPriceSensor.sol";
+import {TestImplementation} from "./utils/TestImplementation.sol";
+
+contract MyTest is Test2 {
struct TokenOptions {
string name;
string symbol;
@@ -33,6 +36,55 @@ contract MangroveTestHelper {
uint gasmax;
uint density;
}
+
+ /* counts the offers of a given pair */
+ function countOffers(
+ AbstractMangrove mgv,
+ address $out,
+ address $in
+ ) public view returns (uint256 count) {
+ uint offerId = mgv.best($out, $in);
+ while (offerId != 0) {
+ count++;
+ (MgvStructs.OfferUnpacked memory ofr, ) = mgv.offerInfo(
+ $out,
+ $in,
+ offerId
+ );
+ offerId = ofr.next;
+ }
+ }
+
+ /* Log OB with console */
+ function printOrderBook(
+ AbstractMangrove mgv,
+ address $out,
+ address $in
+ ) internal view {
+ uint offerId = mgv.best($out, $in);
+ TestToken req_tk = TestToken($in);
+ TestToken ofr_tk = TestToken($out);
+
+ // prettier-ignore
+ console.log(string.concat(unicode"┌────┬──Best offer: ", vm.toString(offerId), unicode"──────"));
+ while (offerId != 0) {
+ (
+ MgvStructs.OfferUnpacked memory ofr,
+ MgvStructs.OfferDetailUnpacked memory detail
+ ) = mgv.offerInfo($out, $in, offerId);
+ console.log(
+ // prettier-ignore
+ string.concat(
+ unicode"│ ", string.concat(offerId < 10 ? " " : "", vm.toString(offerId)), // breaks on id>99
+ unicode" ┆ ", string.concat(toFixed(ofr.wants, req_tk.decimals()), " ", req_tk.symbol()),
+ " / ", string.concat(toFixed(ofr.gives, ofr_tk.decimals()), " ", ofr_tk.symbol()),
+ " ", vm.toString(detail.maker)
+ )
+ );
+ offerId = ofr.next;
+ }
+ console.log(unicode"└────┴─────────────────────");
+ }
}
contract TestTokenHelper is TestToken {
@@ -48,8 +100,8 @@ contract TestTokenHelper is TestToken {
}
}
-contract TestPriceSensor is Test2, MangroveTestHelper {
- ExampleImplementation internal priceSensor;
+contract TestPriceSensor is MyTest {
+ TestImplementation internal priceSensor;
AbstractMangrove internal mgv;
AbstractRouter internal router;
@@ -64,7 +116,7 @@ contract TestPriceSensor is Test2, MangroveTestHelper {
quote: TokenOptions({
name: "Quote Token",
symbol: "QUOTE",
- decimals: 18
+ decimals: 10
}),
defaultFee: 0,
gasprice: 40,
@@ -76,7 +128,7 @@ contract TestPriceSensor is Test2, MangroveTestHelper {
TestTokenHelper internal base;
TestTokenHelper internal quote;
- function setUp() public virtual {
+ function setUp() public {
/**
* @notice Labels map:
*
@@ -137,7 +189,13 @@ contract TestPriceSensor is Test2, MangroveTestHelper {
/**
* Setup price sensor
*/
- priceSensor = new ExampleImplementation(address(mgv), address(this));
+ priceSensor = new TestImplementation(
+ address(mgv),
+ address(this),
+ address(base),
+ address(quote),
+ 50_000
+ );
router = priceSensor.router();
/**
@@ -176,168 +234,67 @@ contract TestPriceSensor is Test2, MangroveTestHelper {
base.approve(address(mgv), mintAmount);
}
- /**
- * UTILS
- */
-
- /* Log OB with console */
- function printOrderBook(address $out, address $in) internal view {
- uint offerId = mgv.best($out, $in);
- TestToken req_tk = TestToken($in);
- TestToken ofr_tk = TestToken($out);
-
- console.log(
- string.concat(
- unicode"┌────┬──Best offer: ",
- vm.toString(offerId),
- unicode"──────"
- )
- );
- while (offerId != 0) {
- (
- MgvStructs.OfferUnpacked memory ofr,
- MgvStructs.OfferDetailUnpacked memory detail
- ) = mgv.offerInfo($out, $in, offerId);
- console.log(
- string.concat(
- unicode"│ ",
- string.concat(offerId < 9 ? " " : "", vm.toString(offerId)), // breaks on id>99
- unicode" ┆ ",
- string.concat(
- toFixed(ofr.wants, req_tk.decimals()),
- " ",
- req_tk.symbol()
- ),
- " / ",
- string.concat(
- toFixed(ofr.gives, ofr_tk.decimals()),
- " ",
- ofr_tk.symbol()
- ),
- " ",
- vm.toString(detail.maker)
- )
+ function setUpOrderBook() external {
+ for (uint i = 0; i < 20; ) {
+ priceSensor.newSensor{value: 1 ether}(
+ 0.005 ether + i * 0.0001 ether,
+ 0.004 ether + i * 0.0001 ether,
+ 0,
+ i
);
- offerId = ofr.next;
- }
- console.log(unicode"└────┴─────────────────────");
- }
-
- function test_newSensor() public {
- address[] memory uniswapPools = new address[](1);
- uniswapPools[0] = address(0);
-
- uint256 offerId = priceSensor.newSensor{value: 0.01 ether}(
- uniswapPools,
- address(base),
- address(quote),
- 0.68 * 1e18
- );
-
- assertEq(offerId, 1);
- }
-
- function test_removePriceSensor() public {
- address[] memory uniswapPools = new address[](1);
- uniswapPools[0] = address(0);
-
- uint256 offerId = priceSensor.newSensor{value: 0.01 ether}(
- uniswapPools,
- address(base),
- address(quote),
- 0.68 * 1e18
- );
-
- uint256 provision = priceSensor.removeSensor(
- address(base),
- address(quote),
- offerId
- );
- assert(provision > 0);
+ unchecked {
+ ++i;
+ }
+ }
}
- // fake uniswap pool
- // and populate its slot0
-
- function test_snipeSensor() public {
+ function test_triggerStopLoss() public {
vm.pauseGasMetering();
+ this.setUpOrderBook();
+ printOrderBook(mgv, address(base), address(quote));
- address[] memory uniswapPools = new address[](0);
- // uniswapPools[0] = ;
-
- // for 0.5 token in we get 1 token out
- uint256 price = 0.5 ether;
- uint256 offerId = priceSensor.newSensor{value: 0.01 ether}(
- uniswapPools,
+ uint256[4][] memory targets = new uint[4][](1);
+ /* offerId */ targets[0][0] = 20;
+ /* takerWants */ targets[0][1] = 0.0059 ether;
+ /* takerGives */ targets[0][2] = 0.0069 ether;
+ /* gasreq_permitted */ targets[0][3] = 50_000;
+ (uint successes, uint takerGot, uint takerGave, , ) = mgv.snipes(
address(base),
address(quote),
- price
+ targets,
+ true
);
- console.log("offerId: %s", offerId);
-
- printOrderBook(address(base), address(quote));
-
- uint256[4][] memory targets = new uint[4][](1);
- /* offerId */ targets[0][0] = offerId;
- /* takerWants */ targets[0][1] = 0.000000000000002 ether;
- /* takerGives */ targets[0][2] = 0.000000000000002 ether;
- /* gasreq_permitted */ targets[0][3] = 30_000;
-
- (
- uint successes,
- uint takerGot,
- uint takerGave,
- uint bounty,
- uint fee
- ) = mgv.snipes(address(base), address(quote), targets, true);
-
assertEq(successes, 1);
+ assertEq(countOffers(mgv, address(base), address(quote)), 19);
+ assertEq(takerGot, 0.0059 ether);
+ assertEq(takerGave, 0.0069 ether);
- console.log("successes: %s", successes);
- console.log("takerGot: %s", takerGot);
- console.log("takerGave: %s", takerGave);
- console.log("bounty: %s", bounty);
- console.log("fee: %s", fee);
-
- printOrderBook(address(base), address(quote));
+ printOrderBook(mgv, address(base), address(quote));
}
- function test_autoRemoveSensorWithoutUniswapPools() public {
+ function test_takeSimpleOffer() public {
vm.pauseGasMetering();
-
- address[] memory uniswapPools = new address[](0);
-
- // for 0.5 token in we get 1 token out
- uint256 price = 0.5 ether;
- uint256 offerId = priceSensor.newSensor{value: 0.01 ether}(
- uniswapPools,
- address(base),
- address(quote),
- price
- );
+ this.setUpOrderBook();
+ printOrderBook(mgv, address(base), address(quote));
uint256[4][] memory targets = new uint[4][](1);
- /* offerId */ targets[0][0] = offerId;
- /* takerWants */ targets[0][1] = 0.000000000000002 ether;
- /* takerGives */ targets[0][2] = 0.000000000000002 ether;
- /* gasreq_permitted */ targets[0][3] = 30_000;
-
- (uint successes, , , , ) = mgv.snipes(
+ /* offerId */ targets[0][0] = 18;
+ /* takerWants */ targets[0][1] = 0.0057 ether;
+ /* takerGives */ targets[0][2] = 0.0067 ether;
+ /* gasreq_permitted */ targets[0][3] = 50_000;
+ (uint successes, uint takerGot, uint takerGave, , ) = mgv.snipes(
address(base),
address(quote),
targets,
true
);
- assertEq(successes, 1);
-
- (MgvStructs.OfferUnpacked memory ofr, ) = mgv.offerInfo(
- address(base),
- address(quote),
- 0
- );
- assertEq(ofr.gives, 0);
+ assertEq(successes, 1);
+ assertEq(countOffers(mgv, address(base), address(quote)), 20);
+ assertEq(takerGot, 0.0057 ether);
+ assertEq(takerGave, 0.0067 ether);
+ printOrderBook(mgv, address(base), address(quote));
}
}
diff --git a/test/utils/TestImplementation.sol b/test/utils/TestImplementation.sol
new file mode 100644
index 0000000..55ec7a7
--- /dev/null
+++ b/test/utils/TestImplementation.sol
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: Unlicense
+pragma solidity ^0.8.10;
+
+import {IMangrove} from "mgv_src/IMangrove.sol";
+import {MgvLib} from "mgv_src/MgvLib.sol";
+import {Direct} from "mgv_src/strategies/offer_maker/abstract/Direct.sol";
+import {SimpleRouter} from "mgv_src/strategies/routers/SimpleRouter.sol";
+import {IERC20} from "mgv_src/IERC20.sol";
+
+import {AbstractPriceSensor} from "../../src/AbstractPriceSensor.sol";
+
+contract TestImplementation is AbstractPriceSensor, Direct {
+ /// The outbound token of the sensor
+ IERC20 private immutable _outbound_tkn;
+ /// The inbound token of the sensor
+ IERC20 private immutable _inbound_tkn;
+
+ /// The gas requirement of the sensor (mostly used for the __callbackOnStopLoss__ function)
+ uint256 private immutable _gasreq;
+
+ constructor(
+ address mgv,
+ address deployer,
+ address outbound_token_,
+ address inbound_token_,
+ uint256 gasreq_
+ )
+ Direct(IMangrove(payable(mgv)), new SimpleRouter(), 100_000, deployer)
+ AbstractPriceSensor(mgv)
+ {
+ router().bind(address(this));
+ _outbound_tkn = IERC20(outbound_token_);
+ _inbound_tkn = IERC20(inbound_token_);
+ _gasreq = gasreq_;
+ }
+
+ function newSensor(
+ uint256 wants,
+ uint256 gives,
+ uint256 gasprice,
+ uint256 pivotId
+ ) public payable returns (uint256 offerId) {
+ (offerId, ) = _newOffer(
+ OfferArgs({
+ outbound_tkn: _outbound_tkn,
+ inbound_tkn: _inbound_tkn,
+ wants: wants,
+ gives: gives,
+ gasreq: _gasreq,
+ gasprice: gasprice,
+ pivotId: pivotId,
+ fund: msg.value,
+ noRevert: false // useful for testing
+ })
+ );
+ }
+
+ function __posthookSuccess__(
+ MgvLib.SingleOrder calldata order,
+ bytes32 makerData
+ ) internal override returns (bytes32) {
+ __callbackOnOfferTaken__(order);
+ return super.__posthookSuccess__(order, makerData);
+ }
+
+ function __callbackOnStopLoss__(
+ MgvLib.SingleOrder calldata order
+ ) internal virtual override {
+ super.__callbackOnStopLoss__(order);
+ }
+}