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); + } +}