diff --git a/.gitmodules b/.gitmodules index 888d42dcd..7892c7d7e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/permit2"] + path = lib/permit2 + url = https://github.com/Uniswap/permit2 diff --git a/addresses/context/goerli.json b/addresses/context/goerli.json index 17fe2c196..9261aa3c7 100644 --- a/addresses/context/goerli.json +++ b/addresses/context/goerli.json @@ -10,5 +10,9 @@ { "address": "0xd77b79be3e85351ff0cbe78f1b58cf8d1064047c", "name": "DAI" + }, + { + "address": "0x000000000022d473030f116ddee9f6b43ac78ba3", + "name": "Permit2" } ] diff --git a/addresses/context/matic.json b/addresses/context/matic.json index bf1c8afeb..35343ba87 100644 --- a/addresses/context/matic.json +++ b/addresses/context/matic.json @@ -30,5 +30,9 @@ { "address": "0x59a424169526ECae25856038598F862043DCeDf7", "name": "MgvGovernance" + }, + { + "address": "0x000000000022d473030f116ddee9f6b43ac78ba3", + "name": "Permit2" } ] diff --git a/addresses/context/maticmum.json b/addresses/context/maticmum.json index e1fdbb9d1..bbe6b932f 100644 --- a/addresses/context/maticmum.json +++ b/addresses/context/maticmum.json @@ -14,5 +14,9 @@ { "address": "0x47897EE61498D02B18794601Ed3A71896A1Ff894", "name": "MgvGovernance" + }, + { + "address": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "name": "Permit2" } ] diff --git a/lib/permit2 b/lib/permit2 new file mode 160000 index 000000000..576f549a7 --- /dev/null +++ b/lib/permit2 @@ -0,0 +1 @@ +Subproject commit 576f549a7351814f112edcc42f3f8472d1712673 diff --git a/script/strategies/mangroveOrder/deployers/MangroveOrderDeployer.s.sol b/script/strategies/mangroveOrder/deployers/MangroveOrderDeployer.s.sol index d174a4118..5fb9bf731 100644 --- a/script/strategies/mangroveOrder/deployers/MangroveOrderDeployer.s.sol +++ b/script/strategies/mangroveOrder/deployers/MangroveOrderDeployer.s.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Unlicense pragma solidity ^0.8.13; +import {IPermit2} from "lib/permit2/src/interfaces/IPermit2.sol"; import {Script, console} from "forge-std/Script.sol"; import {MangroveOrder, IERC20, IMangrove} from "mgv_src/strategies/MangroveOrder.sol"; import {Deployer} from "mgv_script/lib/Deployer.sol"; @@ -17,6 +18,7 @@ contract MangroveOrderDeployer is Deployer { function run() public { innerRun({ mgv: IMangrove(envAddressOrName("MGV", "Mangrove")), + permit2: IPermit2(envAddressOrName("Permit2", "Permit2")), admin: envAddressOrName("MGV_GOVERNANCE", broadcaster()) }); outputDeployment(); @@ -26,7 +28,7 @@ contract MangroveOrderDeployer is Deployer { * @param mgv The Mangrove that MangroveOrder should operate on * @param admin address of the admin on MangroveOrder after deployment */ - function innerRun(IMangrove mgv, address admin) public { + function innerRun(IMangrove mgv, IPermit2 permit2, address admin) public { MangroveOrder mgvOrder; // Bug workaround: Foundry has a bug where the nonce is not incremented when MangroveOrder is deployed. // We therefore ensure that this happens. @@ -36,9 +38,9 @@ contract MangroveOrderDeployer is Deployer { // so setting offer logic's gasreq to 35K is enough // we use 60K here in order to allow partial fills to repost on top of up to 5 identical offers. if (forMultisig) { - mgvOrder = new MangroveOrder{salt:salt}(mgv, admin, 60_000); + mgvOrder = new MangroveOrder{salt:salt}(mgv, permit2, admin, 60_000); } else { - mgvOrder = new MangroveOrder(mgv, admin, 60_000); + mgvOrder = new MangroveOrder(mgv, permit2, admin, 60_000); } // Bug workaround: See comment above `nonce` further up if (nonce == vm.getNonce(broadcaster())) { diff --git a/script/strategies/mangroveOrder/deployers/MumbaiMangroveOrderDeployer.s.sol b/script/strategies/mangroveOrder/deployers/MumbaiMangroveOrderDeployer.s.sol index 26599a20a..5f60a8b3d 100644 --- a/script/strategies/mangroveOrder/deployers/MumbaiMangroveOrderDeployer.s.sol +++ b/script/strategies/mangroveOrder/deployers/MumbaiMangroveOrderDeployer.s.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; +import {IPermit2} from "lib/permit2/src/interfaces/IPermit2.sol"; import {MangroveOrder, IERC20, IMangrove} from "mgv_src/strategies/MangroveOrder.sol"; import {Deployer} from "mgv_script/lib/Deployer.sol"; @@ -19,6 +20,7 @@ contract MumbaiMangroveOrderDeployer is Deployer { function runWithChainSpecificParams() public { new MangroveOrderDeployer().innerRun({ mgv: IMangrove(envAddressOrName("MGV", "Mangrove")), + permit2: IPermit2(envAddressOrName("Permit2", "Permit2")), admin: envAddressOrName("MGV_GOVERNANCE", broadcaster()) }); } diff --git a/script/strategies/mangroveOrder/deployers/PolygonMangroveOrderDeployer.s.sol b/script/strategies/mangroveOrder/deployers/PolygonMangroveOrderDeployer.s.sol index 32359911e..df408dcca 100644 --- a/script/strategies/mangroveOrder/deployers/PolygonMangroveOrderDeployer.s.sol +++ b/script/strategies/mangroveOrder/deployers/PolygonMangroveOrderDeployer.s.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.13; import {Script, console} from "forge-std/Script.sol"; +import {IPermit2} from "lib/permit2/src/interfaces/IPermit2.sol"; import {MangroveOrder, IERC20, IMangrove} from "mgv_src/strategies/MangroveOrder.sol"; import {Deployer} from "mgv_script/lib/Deployer.sol"; @@ -20,6 +21,10 @@ contract PolygonMangroveOrderDeployer is Deployer { function runWithChainSpecificParams() public { mangroveOrderDeployer = new MangroveOrderDeployer(); - mangroveOrderDeployer.innerRun({mgv: IMangrove(fork.get("Mangrove")), admin: fork.get("MgvGovernance")}); + mangroveOrderDeployer.innerRun({ + mgv: IMangrove(fork.get("Mangrove")), + permit2: IPermit2(envAddressOrName("Permit2", "Permit2")), + admin: fork.get("MgvGovernance") + }); } } diff --git a/script/toy/MangroveJs.s.sol b/script/toy/MangroveJs.s.sol index 389e965ab..1a53d82cd 100644 --- a/script/toy/MangroveJs.s.sol +++ b/script/toy/MangroveJs.s.sol @@ -16,6 +16,8 @@ import {IMangrove} from "mgv_src/IMangrove.sol"; import {Deployer} from "mgv_script/lib/Deployer.sol"; import {ActivateMarket} from "mgv_script/core/ActivateMarket.s.sol"; import {PoolAddressProviderMock} from "mgv_script/toy/AaveMock.sol"; +import {IPermit2} from "permit2/src/interfaces/IPermit2.sol"; +import {DeployPermit2} from "permit2/test/utils/DeployPermit2.sol"; import "forge-std/console.sol"; /* @@ -34,8 +36,11 @@ contract MangroveJsDeploy is Deployer { IERC20 public weth; SimpleTestMaker public simpleTestMaker; MangroveOrder public mgo; + IPermit2 public permit2; function run() public { + DeployPermit2 deployPermit2 = new DeployPermit2(); + permit2 = IPermit2(deployPermit2.deployPermit2()); // deploy permit2 using the precompiled bytecode innerRun({gasprice: 1, gasmax: 2_000_000, gasbot: broadcaster()}); outputDeployment(); } @@ -118,7 +123,7 @@ contract MangroveJsDeploy is Deployer { activateMarket.innerRun(mgv, mgvReader, weth, usdc, 1e9, 1e9 / 1000, 0); MangroveOrderDeployer mgoeDeployer = new MangroveOrderDeployer(); - mgoeDeployer.innerRun({admin: broadcaster(), mgv: IMangrove(payable(mgv))}); + mgoeDeployer.innerRun({admin: broadcaster(), mgv: IMangrove(payable(mgv)), permit2: permit2}); address[] memory underlying = dynamic([address(tokenA), address(tokenB), address(dai), address(usdc), address(weth)]); @@ -135,7 +140,7 @@ contract MangroveJsDeploy is Deployer { }); broadcast(); - mgo = new MangroveOrder({mgv: IMangrove(payable(mgv)), deployer: broadcaster(), gasreq:30_000}); + mgo = new MangroveOrder({mgv: IMangrove(payable(mgv)), deployer: broadcaster(), gasreq:30_000, permit2: permit2}); fork.set("MangroveOrder", address(mgo)); } } diff --git a/src/strategies/MangroveOrder.sol b/src/strategies/MangroveOrder.sol index c335f5646..652b1c6e3 100644 --- a/src/strategies/MangroveOrder.sol +++ b/src/strategies/MangroveOrder.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: BSD-2-Clause pragma solidity ^0.8.10; +import {IPermit2} from "lib/permit2/src/interfaces/IPermit2.sol"; +import {ISignatureTransfer} from "lib/permit2/src/interfaces/ISignatureTransfer.sol"; +import {IAllowanceTransfer} from "lib/permit2/src/interfaces/IAllowanceTransfer.sol"; import {IMangrove} from "mgv_src/IMangrove.sol"; import {Forwarder, MangroveOffer} from "mgv_src/strategies/offer_forwarder/abstract/Forwarder.sol"; import {IOrderLogic} from "mgv_src/strategies/interfaces/IOrderLogic.sol"; -import {SimpleRouter} from "mgv_src/strategies/routers/SimpleRouter.sol"; +import {Permit2Router} from "mgv_src/strategies/routers/Permit2Router.sol"; import {TransferLib} from "mgv_src/strategies/utils/TransferLib.sol"; import {MgvLib, IERC20} from "mgv_src/MgvLib.sol"; @@ -23,9 +26,12 @@ contract MangroveOrder is Forwarder, IOrderLogic { ///@notice MangroveOrder is a Forwarder logic with a simple router. ///@param mgv The mangrove contract on which this logic will run taker and maker orders. + ///@param permit2 The permit2 contract ///@param deployer The address of the admin of `this` at the end of deployment ///@param gasreq The gas required for `this` to execute `makerExecute` and `makerPosthook` when called by mangrove for a resting order. - constructor(IMangrove mgv, address deployer, uint gasreq) Forwarder(mgv, new SimpleRouter(), gasreq) { + constructor(IMangrove mgv, IPermit2 permit2, address deployer, uint gasreq) + Forwarder(mgv, new Permit2Router(permit2), gasreq) + { // adding `this` contract to authorized makers of the router before setting admin rights of the router to deployer router().bind(address(this)); router().setAdmin(deployer); @@ -119,8 +125,45 @@ contract MangroveOrder is Forwarder, IOrderLogic { } } - ///@inheritdoc IOrderLogic - function take(TakerOrder calldata tko) external payable returns (TakerOrderResult memory res) { + ///@notice pull inbound_tkn from the msg.sender with permit and then the forward market order to MGV + ///@param outbound_tkn outbound_tkn + ///@param inbound_tkn inbound_tkn + ///@param takerWants Amount of outbound_tkn taker wants + ///@param takerGives Amount of inbound_tkn taker gives + ///@param fillWants isBid + ///@param permit The permit data signed over by the owner + ///@param signature The signature to verify + ///@return totalGot Amount of outbound_tkn received + ///@return totalGave Amount of inbound_tkn received + ///@return totalPenalty Penalty received + ///@return feePaid Fee paid + function marketOrderWithTransferApproval( + IERC20 outbound_tkn, + IERC20 inbound_tkn, + uint takerWants, + uint takerGives, + bool fillWants, + ISignatureTransfer.PermitTransferFrom calldata permit, + bytes calldata signature + ) external returns (uint totalGot, uint totalGave, uint totalPenalty, uint feePaid) { + uint pulled = Permit2Router(address(router())).pull(inbound_tkn, msg.sender, takerGives, true, permit, signature); + require(pulled == takerGives, "mgvOrder/transferInFail"); + (totalGot, totalGave, totalPenalty, feePaid) = + MGV.marketOrder(address(outbound_tkn), address(inbound_tkn), takerWants, takerGives, fillWants); + + uint fund = takerGives - totalGave; + if (fund > 0) { + // refund the sender + (bool noRevert,) = + address(router()).call(abi.encodeWithSelector(router().push.selector, inbound_tkn, msg.sender, fund)); + require(noRevert, "mgvOrder/refundInboundTknFail"); + } + } + + ///@notice take implementation + ///@param tko TakerOrder struct + ///@return res TakerOrderResult Order result + function __take__(TakerOrder calldata tko) internal returns (TakerOrderResult memory res) { // Checking whether order is expired require(tko.expiryDate == 0 || block.timestamp <= tko.expiryDate, "mgvOrder/expired"); @@ -222,6 +265,27 @@ contract MangroveOrder is Forwarder, IOrderLogic { return res; } + ///@inheritdoc IOrderLogic + ///@param tko TakerOrder struct + ///@return TakerOrderResult Result of the take call + function take(TakerOrder calldata tko) external payable returns (TakerOrderResult memory) { + return __take__(tko); + } + + ///@notice call permit2 permit and then call take, this can be used to first approve and then take + ///@param tko TakerOrder struct + ///@param permit The permit data signed over by the owner + ///@param signature The signature to verify + ///@return TakerOrderResult Result of the take call + function takeWithPermit( + TakerOrder calldata tko, + IAllowanceTransfer.PermitSingle memory permit, + bytes calldata signature + ) external payable returns (TakerOrderResult memory) { + Permit2Router(address(router())).permit2().permit(msg.sender, permit, signature); + return __take__(tko); + } + ///@notice logs `OrderSummary` ///@param tko the arguments in memory of the taker order ///@param res the result of the taker order. diff --git a/src/strategies/offer_forwarder/OfferForwarder.sol b/src/strategies/offer_forwarder/OfferForwarder.sol index e8fa3c8bb..b6975c7e5 100644 --- a/src/strategies/offer_forwarder/OfferForwarder.sol +++ b/src/strategies/offer_forwarder/OfferForwarder.sol @@ -3,7 +3,8 @@ pragma solidity ^0.8.10; import {Forwarder, IMangrove, IERC20} from "mgv_src/strategies/offer_forwarder/abstract/Forwarder.sol"; import {ILiquidityProvider} from "mgv_src/strategies/interfaces/ILiquidityProvider.sol"; -import {SimpleRouter, AbstractRouter} from "mgv_src/strategies/routers/SimpleRouter.sol"; +import {SimpleRouter} from "mgv_src/strategies/routers/SimpleRouter.sol"; +import {AbstractRouter} from "mgv_src/strategies/routers/AbstractRouter.sol"; import {MgvLib} from "mgv_src/MgvLib.sol"; contract OfferForwarder is ILiquidityProvider, Forwarder { diff --git a/src/strategies/routers/AbstractRouter.sol b/src/strategies/routers/AbstractRouter.sol index f54daaa92..26bc344c5 100644 --- a/src/strategies/routers/AbstractRouter.sol +++ b/src/strategies/routers/AbstractRouter.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.10; import {AccessControlled} from "mgv_src/strategies/utils/AccessControlled.sol"; import {IERC20} from "mgv_src/MgvLib.sol"; +import {ISignatureTransfer} from "lib/permit2/src/interfaces/ISignatureTransfer.sol"; /// @title AbstractRouter /// @notice Partial implementation and requirements for liquidity routers. diff --git a/src/strategies/routers/Permit2Router.sol b/src/strategies/routers/Permit2Router.sol new file mode 100644 index 000000000..55abee85f --- /dev/null +++ b/src/strategies/routers/Permit2Router.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: BSD-2-Clause +pragma solidity ^0.8.10; + +import {IERC20} from "mgv_src/MgvLib.sol"; +import {IPermit2} from "lib/permit2/src/interfaces/IPermit2.sol"; +import {TransferLib} from "mgv_src/strategies/utils/TransferLib.sol"; +import {ISignatureTransfer} from "lib/permit2/src/interfaces/ISignatureTransfer.sol"; +import {SimpleRouterWithoutGasReq} from "./SimpleRouter.sol"; + +//@title `Permit2Router` instances pull (push) liquidity directly from (to) the an offer owner's account using permit2 contract +//@dev Maker contracts using this router must make sure that the reserve approves the permit2 for all asset that will be pulled (outbound tokens), and then the user needs either approve router inside permit2 or he can use just in time signature to authorize transfer +contract Permit2Router is SimpleRouterWithoutGasReq { + IPermit2 public permit2; + + constructor(IPermit2 _permit2) SimpleRouterWithoutGasReq(74_000) { + permit2 = _permit2; + } + + /// @notice transfers an amount of tokens from the reserve to the maker. + /// @param token Token to be transferred + /// @param owner The account from which the tokens will be transferred. + /// @param amount The amount of tokens to be transferred + /// @param strict wether the caller maker contract wishes to pull at most `amount` tokens of owner. + /// @return pulled The amount pulled if successful (will be equal to `amount`); otherwise, 0. + /// @dev requires approval from `owner` for `this` to transfer `token`. + function __pull__(IERC20 token, address owner, uint amount, bool strict) + internal + virtual + override + returns (uint pulled) + { + amount = strict ? amount : token.balanceOf(owner); + if (TransferLib.transferTokenFromWithPermit2(permit2, token, owner, msg.sender, amount)) { + return amount; + } else { + return 0; + } + } + + ///@notice router-dependent implementation of the `pull` function + ///@param token Token to be transferred + ///@param owner determines the location of the reserve (router implementation dependent). + ///@param amount The amount of tokens to be transferred + ///@param strict wether the caller maker contract wishes to pull at most `amount` tokens of owner. + ///@param transferDetails The spender's requested transfer details for the permitted token + ///@param signature The signature to verify + ///@return pulled The amount pulled if successful; otherwise, 0. + function __pull__( + IERC20 token, + address owner, + uint amount, + bool strict, + ISignatureTransfer.PermitTransferFrom calldata transferDetails, + bytes calldata signature + ) internal returns (uint pulled) { + amount = strict ? amount : token.balanceOf(owner); + if ( + TransferLib.transferTokenFromWithPermit2Signature(permit2, owner, msg.sender, amount, transferDetails, signature) + ) { + return amount; + } else { + return 0; + } + } + + ///@notice pulls liquidity from the reserve and sends it to the calling maker contract. + ///@param token is the ERC20 managing the pulled asset + ///@param reserveId identifies the fund owner (router implementation dependent). + ///@param amount of `token` the maker contract wishes to pull from its reserve + ///@param strict when the calling maker contract accepts to receive more funds from reserve than required (this may happen for gas optimization) + ///@param permit The permit data signed over by the owner + ///@param signature The signature to verify + ///@return pulled the amount that was successfully pulled. + function pull( + IERC20 token, + address reserveId, + uint amount, + bool strict, + ISignatureTransfer.PermitTransferFrom calldata permit, + bytes calldata signature + ) external onlyBound returns (uint pulled) { + if (strict && amount == 0) { + return 0; + } + pulled = __pull__(token, reserveId, amount, strict, permit, signature); + } + + ///@notice router-dependent implementation of the `checkList` function + ///@notice verifies all required approval involving `this` router (either as a spender or owner) + ///@dev `checkList` returns normally if all needed approval are strictly positive. It reverts otherwise with a reason. + ///@param token is the asset whose approval must be checked + ///@param owner the account that requires asset pulling/pushing + function __checkList__(IERC20 token, address owner) internal view virtual override { + // verifying that `this` router can withdraw tokens from owner (required for `withdrawToken` and `pull`) + require(token.allowance(owner, address(permit2)) > 0, "SimpleRouter/NotApprovedByOwner"); + } +} diff --git a/src/strategies/routers/SimpleRouter.sol b/src/strategies/routers/SimpleRouter.sol index 31d1d36df..01aea0b7e 100644 --- a/src/strategies/routers/SimpleRouter.sol +++ b/src/strategies/routers/SimpleRouter.sol @@ -1,57 +1,15 @@ // SPDX-License-Identifier: BSD-2-Clause pragma solidity ^0.8.10; -import {IERC20} from "mgv_src/MgvLib.sol"; -import {TransferLib} from "mgv_src/strategies/utils/TransferLib.sol"; -import {AbstractRouter} from "./AbstractRouter.sol"; +import {SimpleRouterWithoutGasReq} from "./SimpleRouterWithoutGasReq.sol"; ///@title `SimpleRouter` instances pull (push) liquidity directly from (to) the an offer owner's account ///@dev Maker contracts using this router must make sure that the reserve approves the router for all asset that will be pulled (outbound tokens) /// Thus a maker contract using a vault that is not an EOA must make sure this vault has approval capacities. + contract SimpleRouter is - AbstractRouter(70_000) // fails for < 70K with Direct strat + SimpleRouterWithoutGasReq // fails for < 70K with Direct strat { - /// @notice transfers an amount of tokens from the reserve to the maker. - /// @param token Token to be transferred - /// @param owner The account from which the tokens will be transferred. - /// @param amount The amount of tokens to be transferred - /// @param strict wether the caller maker contract wishes to pull at most `amount` tokens of owner. - /// @return pulled The amount pulled if successful (will be equal to `amount`); otherwise, 0. - /// @dev requires approval from `owner` for `this` to transfer `token`. - function __pull__(IERC20 token, address owner, uint amount, bool strict) - internal - virtual - override - returns (uint pulled) - { - // if not strict, pulling all available tokens from reserve - amount = strict ? amount : token.balanceOf(owner); - if (TransferLib.transferTokenFrom(token, owner, msg.sender, amount)) { - return amount; - } else { - return 0; - } - } - - /// @notice transfers an amount of tokens from the maker to the reserve. - /// @inheritdoc AbstractRouter - function __push__(IERC20 token, address owner, uint amount) internal virtual override returns (uint) { - bool success = TransferLib.transferTokenFrom(token, msg.sender, owner, amount); - return success ? amount : 0; - } - - ///@inheritdoc AbstractRouter - function balanceOfReserve(IERC20 token, address owner) public view override returns (uint) { - return token.balanceOf(owner); - } - - ///@notice router-dependent implementation of the `checkList` function - ///@notice verifies all required approval involving `this` router (either as a spender or owner) - ///@dev `checkList` returns normally if all needed approval are strictly positive. It reverts otherwise with a reason. - ///@param token is the asset whose approval must be checked - ///@param owner the account that requires asset pulling/pushing - function __checkList__(IERC20 token, address owner) internal view virtual override { - // verifying that `this` router can withdraw tokens from owner (required for `withdrawToken` and `pull`) - require(token.allowance(owner, address(this)) > 0, "SimpleRouter/NotApprovedByOwner"); - } + ///@notice SimpleRouter empty constructor + constructor() SimpleRouterWithoutGasReq(70_000) {} } diff --git a/src/strategies/routers/SimpleRouterWithoutGasReq.sol b/src/strategies/routers/SimpleRouterWithoutGasReq.sol new file mode 100644 index 000000000..f80d92fbd --- /dev/null +++ b/src/strategies/routers/SimpleRouterWithoutGasReq.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: BSD-2-Clause +pragma solidity ^0.8.10; + +import {IERC20} from "mgv_src/MgvLib.sol"; +import {TransferLib} from "mgv_src/strategies/utils/TransferLib.sol"; +import {AbstractRouter} from "./AbstractRouter.sol"; +import {ISignatureTransfer} from "lib/permit2/src/interfaces/ISignatureTransfer.sol"; + +///@title `SimpleRouterWithoutGasReq` instances pull (push) liquidity directly from (to) the an offer owner's account +///@dev Maker contracts using this router must make sure that the reserve approves the router for all asset that will be pulled (outbound tokens) +/// Thus a maker contract using a vault that is not an EOA must make sure this vault has approval capacities. + +contract SimpleRouterWithoutGasReq is AbstractRouter { + ///@notice SimpleRouterWithoutGasReq constructor + ///@param gasreq Estimated gas req for router + constructor(uint gasreq) AbstractRouter(gasreq) {} + + /// @notice transfers an amount of tokens from the reserve to the maker. + /// @param token Token to be transferred + /// @param owner The account from which the tokens will be transferred. + /// @param amount The amount of tokens to be transferred + /// @param strict wether the caller maker contract wishes to pull at most `amount` tokens of owner. + /// @return pulled The amount pulled if successful (will be equal to `amount`); otherwise, 0. + /// @dev requires approval from `owner` for `this` to transfer `token`. + function __pull__(IERC20 token, address owner, uint amount, bool strict) + internal + virtual + override + returns (uint pulled) + { + // if not strict, pulling all available tokens from reserve + amount = strict ? amount : token.balanceOf(owner); + if (TransferLib.transferTokenFrom(token, owner, msg.sender, amount)) { + return amount; + } else { + return 0; + } + } + + /// @notice transfers an amount of tokens from the maker to the reserve. + /// @inheritdoc AbstractRouter + function __push__(IERC20 token, address owner, uint amount) internal virtual override returns (uint) { + bool success = TransferLib.transferTokenFrom(token, msg.sender, owner, amount); + return success ? amount : 0; + } + + ///@inheritdoc AbstractRouter + function balanceOfReserve(IERC20 token, address owner) public view override returns (uint) { + return token.balanceOf(owner); + } + + ///@notice router-dependent implementation of the `checkList` function + ///@notice verifies all required approval involving `this` router (either as a spender or owner) + ///@dev `checkList` returns normally if all needed approval are strictly positive. It reverts otherwise with a reason. + ///@param token is the asset whose approval must be checked + ///@param owner the account that requires asset pulling/pushing + function __checkList__(IERC20 token, address owner) internal view virtual override { + // verifying that `this` router can withdraw tokens from owner (required for `withdrawToken` and `pull`) + require(token.allowance(owner, address(this)) > 0, "SimpleRouter/NotApprovedByOwner"); + } +} diff --git a/src/strategies/utils/TransferLib.sol b/src/strategies/utils/TransferLib.sol index c9a272dea..6a851b146 100644 --- a/src/strategies/utils/TransferLib.sol +++ b/src/strategies/utils/TransferLib.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.10; import {IERC20} from "mgv_src/MgvLib.sol"; +import {IPermit2} from "lib/permit2/src/interfaces/IPermit2.sol"; +import {ISignatureTransfer} from "lib/permit2/src/interfaces/ISignatureTransfer.sol"; ///@title This library helps with safely interacting with ERC20 tokens ///@notice Transferring 0 or to self will be skipped. @@ -57,6 +59,71 @@ library TransferLib { return _transferTokenFrom(token, spender, recipient, amount); } + ///@notice This transfer amount of token to recipient address from spender address + ///@param permit2 Permit2 contract + ///@param token Token to be transferred + ///@param spender Address of the spender, where the tokens will be transferred from + ///@param recipient Address of the recipient, where the tokens will be transferred to + ///@param amount The amount of tokens to be transferred + ///@return true if transfer was successful; otherwise, false. + function transferTokenFromWithPermit2(IPermit2 permit2, IERC20 token, address spender, address recipient, uint amount) + internal + returns (bool) + { + if (amount == 0) { + return true; + } + if (spender == recipient) { + return token.balanceOf(spender) >= amount; + } + + (bool success,) = address(permit2).call( + abi.encodeWithSignature( + "transferFrom(address,address,uint160,address)", + address(spender), + address(recipient), + uint160(amount), + address(token) + ) + ); + return success; + } + + ///@notice This transfer amount of token to recipient address from spender address + ///@param permit2 Permit2 contract + ///@param spender Address of the spender, where the tokens will be transferred from + ///@param recipient Address of the recipient, where the tokens will be transferred to + ///@param amount The amount of tokens to be transferred spender, where the tokens will be transferred from + ///@param permit The permit data signed over by the owner + ///@param signature The signature to verify + ///@return true if transfer was successful; otherwise, false. + function transferTokenFromWithPermit2Signature( + IPermit2 permit2, + address spender, + address recipient, + uint amount, + ISignatureTransfer.PermitTransferFrom calldata permit, + bytes calldata signature + ) internal returns (bool) { + if (amount == 0) { + return true; + } + if (spender == recipient) { + return IERC20(permit.permitted.token).balanceOf(spender) >= amount; + } + + (bool success,) = address(permit2).call( + abi.encodeWithSignature( + "permitTransferFrom(((address,uint256),uint256,uint256),(address,uint256),address,bytes)", + permit, + ISignatureTransfer.SignatureTransferDetails({to: recipient, requestedAmount: amount}), + spender, + signature + ) + ); + return success; + } + ///@notice This transfer amount of token to recipient address from spender address ///@param token Token to be transferred ///@param spender Address of the spender, where the tokens will be transferred from diff --git a/test/lib/permit2/permit2Helpers.sol b/test/lib/permit2/permit2Helpers.sol new file mode 100644 index 000000000..8ab035c1f --- /dev/null +++ b/test/lib/permit2/permit2Helpers.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.10; + +import "forge-std/Test.sol"; +import {ISignatureTransfer} from "lib/permit2/src/interfaces/ISignatureTransfer.sol"; +import {IAllowanceTransfer} from "lib/permit2/src/interfaces/IAllowanceTransfer.sol"; +import {PermitSignature} from "lib/permit2/test/utils/PermitSignature.sol"; + +contract Permit2Helpers is Test, PermitSignature { + function getPermitTransferSignatureWithSpecifiedAddress( + ISignatureTransfer.PermitTransferFrom memory permit, + uint privateKey, + bytes32 domainSeparator, + address addr + ) internal pure returns (bytes memory sig) { + bytes32 tokenPermissions = keccak256(abi.encode(_TOKEN_PERMISSIONS_TYPEHASH, permit.permitted)); + bytes32 msgHash = keccak256( + abi.encodePacked( + "\x19\x01", + domainSeparator, + keccak256(abi.encode(_PERMIT_TRANSFER_FROM_TYPEHASH, tokenPermissions, addr, permit.nonce, permit.deadline)) + ) + ); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, msgHash); + return bytes.concat(r, s, bytes1(v)); + } + + function getPermitTransferFrom(address token, uint amount, uint nonce, uint deadline) + internal + pure + returns (ISignatureTransfer.PermitTransferFrom memory) + { + return ISignatureTransfer.PermitTransferFrom({ + permitted: ISignatureTransfer.TokenPermissions({token: token, amount: amount}), + nonce: nonce, + deadline: deadline + }); + } + + function getPermit(address token, uint160 amount, uint48 expiration, uint48 nonce, address spender) + internal + pure + returns (IAllowanceTransfer.PermitSingle memory) + { + IAllowanceTransfer.PermitDetails memory permitDetails = + IAllowanceTransfer.PermitDetails({token: address(token), amount: amount, expiration: expiration, nonce: nonce}); + IAllowanceTransfer.PermitSingle memory permit = + IAllowanceTransfer.PermitSingle({details: permitDetails, spender: address(spender), sigDeadline: expiration}); + + return permit; + } +} diff --git a/test/script/core/deployers/PolygonMangroveDeployer.t.sol b/test/script/core/deployers/PolygonMangroveDeployer.t.sol index 5c404dd15..7290c7ee8 100644 --- a/test/script/core/deployers/PolygonMangroveDeployer.t.sol +++ b/test/script/core/deployers/PolygonMangroveDeployer.t.sol @@ -14,9 +14,14 @@ import {MgvReader} from "mgv_src/periphery/MgvReader.sol"; import {MgvCleaner} from "mgv_src/periphery/MgvCleaner.sol"; import {MgvOracle} from "mgv_src/periphery/MgvOracle.sol"; import {IMangrove} from "mgv_src/IMangrove.sol"; +import {DeployPermit2} from "permit2/test/utils/DeployPermit2.sol"; contract PolygonMangroveDeployerTest is BaseMangroveDeployerTest { function setUp() public { + DeployPermit2 deployPermit2 = new DeployPermit2(); + address permit2 = deployPermit2.deployPermit2(); + fork.set("Permit2", permit2); + chief = freshAddress("MgvGovernance"); fork.set("MgvGovernance", chief); gasbot = freshAddress("Gasbot"); diff --git a/test/script/strategies/mangroveOrder/deployers/BaseMangroveOrderDeployer.t.sol b/test/script/strategies/mangroveOrder/deployers/BaseMangroveOrderDeployer.t.sol index e343dd808..a74172b0f 100644 --- a/test/script/strategies/mangroveOrder/deployers/BaseMangroveOrderDeployer.t.sol +++ b/test/script/strategies/mangroveOrder/deployers/BaseMangroveOrderDeployer.t.sol @@ -5,6 +5,7 @@ import {Deployer} from "mgv_script/lib/Deployer.sol"; import {MangroveDeployer} from "mgv_script/core/deployers/MangroveDeployer.s.sol"; import {Test2, Test} from "mgv_lib/Test2.sol"; +import {IPermit2} from "permit2/src/interfaces/IPermit2.sol"; import {MgvStructs} from "mgv_src/MgvLib.sol"; import {Mangrove} from "mgv_src/Mangrove.sol"; @@ -18,6 +19,9 @@ import { MangroveOrder } from "mgv_script/strategies/mangroveOrder/deployers/MangroveOrderDeployer.s.sol"; +import {IPermit2} from "permit2/src/interfaces/IPermit2.sol"; +import {DeployPermit2} from "permit2/test/utils/DeployPermit2.sol"; + /** * Base test suite for [Chain]MangroveOrderDeployer scripts */ @@ -28,7 +32,8 @@ abstract contract BaseMangroveOrderDeployerTest is Deployer, Test2 { function test_normal_deploy() public { // MangroveOrder - verify mgv is used and admin is chief address mgv = fork.get("Mangrove"); - mgoDeployer.innerRun(IMangrove(payable(mgv)), chief); + IPermit2 permit2 = IPermit2(fork.get("Permit2")); + mgoDeployer.innerRun(IMangrove(payable(mgv)), IPermit2(permit2), chief); MangroveOrder mgoe = MangroveOrder(fork.get("MangroveOrder")); address mgvOrderRouter = fork.get("MangroveOrder-Router"); diff --git a/test/script/strategies/mangroveOrder/deployers/MangroveOrderDeployer.t.sol b/test/script/strategies/mangroveOrder/deployers/MangroveOrderDeployer.t.sol index 2799591b5..0b97608c1 100644 --- a/test/script/strategies/mangroveOrder/deployers/MangroveOrderDeployer.t.sol +++ b/test/script/strategies/mangroveOrder/deployers/MangroveOrderDeployer.t.sol @@ -19,9 +19,14 @@ import {MgvCleaner} from "mgv_src/periphery/MgvCleaner.sol"; import {MgvOracle} from "mgv_src/periphery/MgvOracle.sol"; import {IMangrove} from "mgv_src/IMangrove.sol"; import {AbstractRouter} from "mgv_src/strategies/routers/AbstractRouter.sol"; +import {DeployPermit2} from "permit2/test/utils/DeployPermit2.sol"; contract MangroveOrderDeployerTest is BaseMangroveOrderDeployerTest { function setUp() public { + DeployPermit2 deployPermit2 = new DeployPermit2(); + address permit2 = deployPermit2.deployPermit2(); + fork.set("Permit2", permit2); + chief = freshAddress("admin"); address gasbot = freshAddress("gasbot"); diff --git a/test/script/strategies/mangroveOrder/deployers/PolygonMangroveOrderDeployer.t.sol b/test/script/strategies/mangroveOrder/deployers/PolygonMangroveOrderDeployer.t.sol index d513dd440..7d9340862 100644 --- a/test/script/strategies/mangroveOrder/deployers/PolygonMangroveOrderDeployer.t.sol +++ b/test/script/strategies/mangroveOrder/deployers/PolygonMangroveOrderDeployer.t.sol @@ -19,9 +19,14 @@ import { PolygonMangroveOrderDeployer, MangroveOrder } from "mgv_script/strategies/mangroveOrder/deployers/PolygonMangroveOrderDeployer.s.sol"; +import {DeployPermit2} from "permit2/test/utils/DeployPermit2.sol"; contract PolygonMangroveOrderDeployerTest is BaseMangroveOrderDeployerTest { function setUp() public { + DeployPermit2 deployPermit2 = new DeployPermit2(); + address permit2 = deployPermit2.deployPermit2(); + fork.set("Permit2", permit2); + chief = freshAddress("chief"); fork.set("MgvGovernance", chief); diff --git a/test/strategies/MgvOrder.t.sol b/test/strategies/MgvOrder.t.sol index 80f2025ad..c4f3016cc 100644 --- a/test/strategies/MgvOrder.t.sol +++ b/test/strategies/MgvOrder.t.sol @@ -3,19 +3,39 @@ pragma solidity ^0.8.10; import {MangroveTest, MgvReader, TestMaker, TestTaker, TestSender, console} from "mgv_test/lib/MangroveTest.sol"; import {IMangrove} from "mgv_src/IMangrove.sol"; -import {MangroveOrder as MgvOrder, SimpleRouter} from "mgv_src/strategies/MangroveOrder.sol"; +import {MangroveOrder as MgvOrder} from "mgv_src/strategies/MangroveOrder.sol"; +import {SimpleRouter} from "mgv_src/strategies/routers/SimpleRouter.sol"; +import {Permit2Router} from "mgv_src/strategies/routers/Permit2Router.sol"; import {PinnedPolygonFork} from "mgv_test/lib/forks/Polygon.sol"; import {TransferLib} from "mgv_src/strategies/utils/TransferLib.sol"; import {IOrderLogic} from "mgv_src/strategies/interfaces/IOrderLogic.sol"; import {MgvStructs, MgvLib, IERC20} from "mgv_src/MgvLib.sol"; import {TestToken} from "mgv_test/lib/tokens/TestToken.sol"; +import {IPermit2} from "permit2/src/interfaces/IPermit2.sol"; +import {DeployPermit2} from "permit2/test/utils/DeployPermit2.sol"; +import {ISignatureTransfer} from "lib/permit2/src/interfaces/ISignatureTransfer.sol"; +import {IAllowanceTransfer} from "lib/permit2/src/interfaces/IAllowanceTransfer.sol"; +import {Permit2Helpers} from "mgv_test/lib/permit2/permit2Helpers.sol"; -contract MangroveOrder_Test is MangroveTest { +contract MangroveOrder_Test is MangroveTest, DeployPermit2, Permit2Helpers { uint constant GASREQ = 35_000; + bytes32 DOMAIN_SEPARATOR; + uint48 EXPIRATION; + uint48 NONCE; + // to check ERC20 logging event Transfer(address indexed from, address indexed to, uint value); + event Permit( + address indexed owner, + address indexed token, + address indexed spender, + uint160 amount, + uint48 expiration, + uint48 nonce + ); + event OrderSummary( IMangrove mangrove, IERC20 indexed outbound_tkn, @@ -34,6 +54,7 @@ contract MangroveOrder_Test is MangroveTest { uint restingOrderId ); + IPermit2 internal permit2; MgvOrder internal mgo; TestMaker internal ask_maker; TestMaker internal bid_maker; @@ -58,8 +79,12 @@ contract MangroveOrder_Test is MangroveTest { quote = TestToken(fork.get("DAI")); setupMarket(base, quote); + permit2 = IPermit2(deployPermit2()); + DOMAIN_SEPARATOR = permit2.DOMAIN_SEPARATOR(); + EXPIRATION = uint48(block.timestamp + 1000); + NONCE = 0; // this contract is admin of MgvOrder and its router - mgo = new MgvOrder(IMangrove(payable(mgv)), $(this), GASREQ); + mgo = new MgvOrder(IMangrove(payable(mgv)), permit2, $(this), GASREQ); // mgvOrder needs to approve mangrove for inbound & outbound token transfer (inbound when acting as a taker, outbound when matched as a maker) IERC20[] memory tokens = new IERC20[](2); tokens[0] = base; @@ -71,8 +96,11 @@ contract MangroveOrder_Test is MangroveTest { deal($(quote), $(this), 10_000 ether); // user approves `mgo` to pull quote or base when doing a market order - TransferLib.approveToken(quote, $(mgo.router()), 10_000 ether); - TransferLib.approveToken(base, $(mgo.router()), 10 ether); + TransferLib.approveToken(base, address(permit2), 10 ether); + TransferLib.approveToken(quote, address(permit2), 10_000 ether); + + permit2.approve(address(base), address(mgo.router()), type(uint160).max, type(uint48).max); + permit2.approve(address(quote), address(mgo.router()), type(uint160).max, type(uint48).max); // `sell_taker` will take resting bid sell_taker = setupTaker($(quote), $(base), "sell-taker"); @@ -156,17 +184,33 @@ contract MangroveOrder_Test is MangroveTest { assertEq(mgv.governance(), mgo.admin(), "Invalid admin address"); } - function freshTaker(uint balBase, uint balQuote) internal returns (address fresh_taker) { - fresh_taker = freshAddress("MgvOrderTester"); + function __freshTaker__(uint balBase, uint balQuote, address fresh_taker) internal { deal($(quote), fresh_taker, balQuote); deal($(base), fresh_taker, balBase); deal(fresh_taker, 1 ether); + vm.startPrank(fresh_taker); - quote.approve(address(mgo.router()), type(uint).max); - base.approve(address(mgo.router()), type(uint).max); + // always unlimitted approval permit2 + quote.approve(address(permit2), type(uint).max); + base.approve(address(permit2), type(uint).max); vm.stopPrank(); } + function freshTaker(uint balBase, uint balQuote) internal returns (address fresh_taker) { + fresh_taker = freshAddress("MgvOrderTester"); + __freshTaker__(balBase, balQuote, fresh_taker); + // allow router to pull funds from permit2 + vm.startPrank(fresh_taker); + permit2.approve(address(base), address(mgo.router()), type(uint160).max, type(uint48).max); + permit2.approve(address(quote), address(mgo.router()), type(uint160).max, type(uint48).max); + vm.stopPrank(); + } + + function freshTakerForPermit2(uint balBase, uint balQuote, uint privKey) internal returns (address fresh_taker) { + fresh_taker = vm.addr(privKey); + __freshTaker__(balBase, balQuote, fresh_taker); + } + //////////////////////// /// Tests taker side /// //////////////////////// @@ -628,7 +672,8 @@ contract MangroveOrder_Test is MangroveTest { sender.refuseNative(); vm.startPrank($(sender)); - TransferLib.approveToken(base, $(mgo.router()), type(uint).max); + TransferLib.approveToken(base, address(permit2), type(uint).max); + permit2.approve(address(base), address(mgo.router()), type(uint160).max, type(uint48).max); vm.stopPrank(); // mocking MangroveOrder failure to post resting offer vm.mockCall($(mgv), abi.encodeWithSelector(mgv.newOffer.selector), abi.encode(uint(0))); @@ -829,4 +874,86 @@ contract MangroveOrder_Test is MangroveTest { assertTrue(successes == 1, "Snipe failed"); assertTrue(mgv.offers($(quote), $(base), cold_buyResult.offerId).gives() > 0, "Update failed"); } + + function test_empty_fill_buy_with_resting_order_is_correctly_posted_with_permit2_approvals() public { + IOrderLogic.TakerOrder memory buyOrder = IOrderLogic.TakerOrder({ + outbound_tkn: base, + inbound_tkn: quote, + fillOrKill: false, + fillWants: true, + takerWants: 1 ether, + takerGives: 1998 ether, + restingOrder: true, + pivotId: 0, + expiryDate: 0 //NA + }); + + IOrderLogic.TakerOrderResult memory expectedResult = + IOrderLogic.TakerOrderResult({takerGot: 0, takerGave: 0, bounty: 0, fee: 0, offerId: 5}); + + uint privKey = 0x1234; + address fresh_taker = freshTakerForPermit2(0, 1998 ether, privKey); + + // generate permit to just in time approval + IAllowanceTransfer.PermitSingle memory permit = + getPermit(address(buyOrder.inbound_tkn), uint160(buyOrder.takerGives), EXPIRATION, NONCE, address(mgo.router())); + + bytes memory signature = getPermitSignature(permit, privKey, DOMAIN_SEPARATOR); + uint nativeBalBefore = fresh_taker.balance; + + // checking log emission + expectFrom(address(permit2)); + emit Permit( + fresh_taker, address(buyOrder.inbound_tkn), address(mgo.router()), uint160(buyOrder.takerGives), EXPIRATION, NONCE + ); + + expectFrom($(mgo)); + logOrderData(IMangrove(payable(mgv)), fresh_taker, buyOrder, expectedResult); + + vm.prank(fresh_taker); + IOrderLogic.TakerOrderResult memory res = mgo.takeWithPermit{value: 0.1 ether}(buyOrder, permit, signature); + + assertTrue(res.offerId > 0, "Offer not posted"); + assertEq(fresh_taker.balance, nativeBalBefore - 0.1 ether, "Value not deposited"); + assertEq(mgo.provisionOf(quote, base, res.offerId), 0.1 ether, "Offer not provisioned"); + // checking mappings + assertEq(mgo.ownerOf(quote, base, res.offerId), fresh_taker, "Invalid offer owner"); + assertEq(quote.balanceOf(fresh_taker), 1998 ether, "Incorrect remaining quote balance"); + assertEq(base.balanceOf(fresh_taker), 0, "Incorrect obtained base balance"); + // checking price of offer + MgvStructs.OfferPacked offer = mgv.offers($(quote), $(base), res.offerId); + MgvStructs.OfferDetailPacked detail = mgv.offerDetails($(quote), $(base), res.offerId); + assertEq(offer.gives(), 1998 ether, "Incorrect offer gives"); + assertEq(offer.wants(), 1 ether, "Incorrect offer wants"); + assertEq(offer.prev(), 0, "Offer should be best of the book"); + assertEq(detail.maker(), address(mgo), "Incorrect maker"); + } + + function test_empty_market_order_with_permit2_approvals() public { + uint takerWants = 1 ether; + uint takerGives = 1998 ether; + bool fillWants = true; + + uint privKey = 0x1234; + address fresh_taker = freshTakerForPermit2(0, takerGives, privKey); + // generate transfer permit for just in time approval + + ISignatureTransfer.PermitTransferFrom memory transferDetails = + getPermitTransferFrom(address(quote), takerGives, NONCE, EXPIRATION); + + bytes memory signature = + getPermitTransferSignatureWithSpecifiedAddress(transferDetails, privKey, DOMAIN_SEPARATOR, address(mgo.router())); + + expectFrom(address(quote)); + emit Transfer(fresh_taker, address(mgo), takerGives); + + vm.prank(fresh_taker); + (uint takerGot, uint takerGave, uint bounty, uint fee) = + mgo.marketOrderWithTransferApproval(base, quote, takerWants, takerGives, fillWants, transferDetails, signature); + + assertEq(takerGot, 0 ether, "Incorrect taker got"); + assertEq(takerGave, 0, "Incorrect taker gave"); + assertEq(bounty, 0, "Offer bounty"); + assertEq(fee, 0, "Offer fee"); + } } diff --git a/test/strategies/unit/MangroveOffer.t.sol b/test/strategies/unit/MangroveOffer.t.sol index d9d509ee5..38817e6d3 100644 --- a/test/strategies/unit/MangroveOffer.t.sol +++ b/test/strategies/unit/MangroveOffer.t.sol @@ -3,7 +3,8 @@ pragma solidity ^0.8.10; import "mgv_test/lib/MangroveTest.sol"; import {DirectTester, IMangrove, IERC20} from "mgv_src/strategies/offer_maker/DirectTester.sol"; -import {SimpleRouter, AbstractRouter} from "mgv_src/strategies/routers/SimpleRouter.sol"; +import {SimpleRouter} from "mgv_src/strategies/routers/SimpleRouter.sol"; +import {AbstractRouter} from "mgv_src/strategies/routers/AbstractRouter.sol"; contract MangroveOfferTest is MangroveTest { TestToken weth; diff --git a/test/strategies/unit/routers/Permit2Router.t.sol b/test/strategies/unit/routers/Permit2Router.t.sol new file mode 100644 index 000000000..655cce11b --- /dev/null +++ b/test/strategies/unit/routers/Permit2Router.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.10; + +import "../OfferLogic.t.sol"; +import {Permit2Router} from "mgv_src/strategies/routers/Permit2Router.sol"; +import {IPermit2} from "lib/permit2/src/interfaces/IPermit2.sol"; +import {DeployPermit2} from "permit2/test/utils/DeployPermit2.sol"; + +contract Permit2RouterTest is OfferLogicTest, DeployPermit2 { + Permit2Router router; + + IPermit2 permit2; + + function setupLiquidityRouting() internal override { + // OfferMaker has no router, replacing 0x router by a Permit2Router + permit2 = IPermit2(deployPermit2()); + router = new Permit2Router(permit2); + router.bind(address(makerContract)); + // maker must approve router + vm.prank(deployer); + makerContract.setRouter(router); + + vm.startPrank(owner); + weth.approve(address(permit2), type(uint).max); + usdc.approve(address(permit2), type(uint).max); + permit2.approve(address(weth), address(router), type(uint160).max, type(uint48).max); + permit2.approve(address(usdc), address(router), type(uint160).max, type(uint48).max); + vm.stopPrank(); + } + + function fundStrat() internal virtual override { + deal($(weth), owner, 1 ether); + deal($(usdc), owner, cash(usdc, 2000)); + } + + event MakerBind(address indexed maker); + event MakerUnbind(address indexed maker); + + function test_admin_can_unbind() public { + expectFrom(address(router)); + emit MakerUnbind(address(makerContract)); + router.unbind(address(makerContract)); + } + + function test_maker_can_unbind() public { + expectFrom(address(router)); + emit MakerUnbind(address(makerContract)); + vm.prank(address(makerContract)); + router.unbind(); + } +} diff --git a/test/strategies/unit/routers/Permit2RouterSignature.t.sol b/test/strategies/unit/routers/Permit2RouterSignature.t.sol new file mode 100644 index 000000000..27f660181 --- /dev/null +++ b/test/strategies/unit/routers/Permit2RouterSignature.t.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.10; + +import {TestToken} from "mgv_test/lib/tokens/TestToken.sol"; +import {MangroveTest} from "mgv_test/lib/MangroveTest.sol"; +import {Permit2Router} from "mgv_src/strategies/routers/Permit2Router.sol"; +import {ISignatureTransfer} from "lib/permit2/src/interfaces/ISignatureTransfer.sol"; +import {IAllowanceTransfer} from "lib/permit2/src/interfaces/IAllowanceTransfer.sol"; +import {IPermit2} from "lib/permit2/src/interfaces/IPermit2.sol"; +import {DeployPermit2} from "permit2/test/utils/DeployPermit2.sol"; +import {Permit2Helpers} from "mgv_test/lib/permit2/permit2Helpers.sol"; + +contract Permit2RouterSignatureTest is MangroveTest, DeployPermit2, Permit2Helpers { + address owner; + uint ownerPrivateKey; + TestToken weth; + TestToken usdc; + uint48 NONCE = 0; + + bytes32 DOMAIN_SEPARATOR; + uint48 EXPIRATION; + uint160 AMOUNT = 25; + + Permit2Router router; + IPermit2 permit2; + + function setUp() public virtual override { + super.setUp(); + ownerPrivateKey = 0x12341234; + owner = vm.addr(ownerPrivateKey); + + weth = new TestToken(owner, "WETH", "WETH", 18); + usdc = new TestToken(owner, "USDC", "USDC", 18); + permit2 = IPermit2(deployPermit2()); + DOMAIN_SEPARATOR = permit2.DOMAIN_SEPARATOR(); + + router = new Permit2Router(permit2); + + EXPIRATION = uint48(block.timestamp + 1000); + + router.bind(address(this)); + + deal($(weth), owner, cash(weth, 50)); + + vm.startPrank(owner); + weth.approve(address(permit2), type(uint).max); + usdc.approve(address(permit2), type(uint).max); + permit2.approve(address(weth), address(router), type(uint160).max, type(uint48).max); + permit2.approve(address(usdc), address(router), type(uint160).max, type(uint48).max); + vm.stopPrank(); + } + + function test_pull_with_signature_transfer() public { + ISignatureTransfer.PermitTransferFrom memory transferDetails = + getPermitTransferFrom(address(weth), AMOUNT, NONCE, EXPIRATION); + bytes memory sig = getPermitTransferSignatureWithSpecifiedAddress( + transferDetails, ownerPrivateKey, DOMAIN_SEPARATOR, address(router) + ); + + uint startBalanceFrom = weth.balanceOf(owner); + uint startBalanceTo = weth.balanceOf(address(this)); + + router.pull(weth, owner, AMOUNT, true, transferDetails, sig); + + assertEq(weth.balanceOf(owner), startBalanceFrom - AMOUNT); + assertEq(weth.balanceOf(address(this)), startBalanceTo + AMOUNT); + } + + function test_pull_with_permit() public { + IAllowanceTransfer.PermitSingle memory permit = getPermit(address(weth), AMOUNT, EXPIRATION, NONCE, address(router)); + bytes memory sig = getPermitSignature(permit, ownerPrivateKey, DOMAIN_SEPARATOR); + + uint startBalanceFrom = weth.balanceOf(owner); + uint startBalanceTo = weth.balanceOf(address(this)); + + permit2.permit(owner, permit, sig); + router.pull(weth, owner, AMOUNT, true); + + assertEq(weth.balanceOf(owner), startBalanceFrom - AMOUNT); + assertEq(weth.balanceOf(address(this)), startBalanceTo + AMOUNT); + } +}