From 9c5c948e3059367185a59f4890e0841f0999283a Mon Sep 17 00:00:00 2001 From: panukettu Date: Mon, 20 Nov 2023 14:15:03 +0200 Subject: [PATCH 1/2] feat: support uni v3 exact input and return operation outputs from the multicall --- src/contracts/core/periphery/KrMulticall.sol | 94 ++++++++++++++++---- src/contracts/test/Periphery.t.sol | 39 -------- 2 files changed, 75 insertions(+), 58 deletions(-) diff --git a/src/contracts/core/periphery/KrMulticall.sol b/src/contracts/core/periphery/KrMulticall.sol index fb0f163e..baa58baa 100644 --- a/src/contracts/core/periphery/KrMulticall.sol +++ b/src/contracts/core/periphery/KrMulticall.sol @@ -9,6 +9,18 @@ import {IVaultExtender} from "vault/interfaces/IVaultExtender.sol"; import {IERC20} from "kresko-lib/token/IERC20.sol"; import {IKreskoAsset} from "kresko-asset/IKreskoAsset.sol"; +interface ISwapRouter { + struct ExactInputParams { + bytes path; + address recipient; + uint256 deadline; + uint256 amountIn; + uint256 amountOutMinimum; + } + + function exactInput(ExactInputParams memory params) external returns (uint256 amountOut); +} + // solhint-disable avoid-low-level-calls, code-complexity contract KrMulticall { struct Op { @@ -18,11 +30,20 @@ contract KrMulticall { struct OpData { address tokenIn; - uint96 amountIn; address tokenOut; + uint96 amountIn; uint96 amountOut; - uint128 amountMin; + uint128 amountOutMin; uint128 index; + uint256 deadline; + bytes path; + } + + struct OpResult { + address tokenIn; + uint256 amountIn; + address tokenOut; + uint256 amountOut; } enum OpAction { @@ -38,8 +59,7 @@ contract KrMulticall { SynthWrap, VaultDeposit, VaultRedeem, - AMMIn, - AMMOut + AMMExactInput } error NoAllowance(OpAction action, address token, string symbol); @@ -49,28 +69,45 @@ contract KrMulticall { address public kresko; address public kiss; - address public amm; + ISwapRouter public uniswapRouter; - constructor(address _kresko, address _kiss, address _amm) { + constructor(address _kresko, address _kiss, address _uniswapRouter) { kresko = _kresko; kiss = _kiss; - amm = _amm; + uniswapRouter = ISwapRouter(_uniswapRouter); } - function execute(Op[] calldata ops, bytes calldata rsPayload) external payable { + function execute(Op[] calldata ops, bytes calldata rsPayload) external payable returns (OpResult[] memory results) { unchecked { + results = new OpResult[](ops.length); for (uint256 i; i < ops.length; i++) { Op calldata op = ops[i]; - IERC20 tokenIn = IERC20(op.data.tokenIn); - if (address(tokenIn) != address(0)) { + if (op.data.tokenIn != address(0)) { + IERC20 tokenIn = IERC20(op.data.tokenIn); + results[i].tokenIn = op.data.tokenIn; + + uint256 balIn = tokenIn.balanceOf(msg.sender); _pullTokenIn(op); + results[i].amountIn = balIn - tokenIn.balanceOf(msg.sender); + } + + if (op.data.tokenOut != address(0)) { + results[i].tokenOut = op.data.tokenOut; + results[i].amountOut = IERC20(op.data.tokenOut).balanceOf(msg.sender); } (bool success, bytes memory returndata) = _handleOp(ops[i], rsPayload); if (!success) _revert(returndata); - _handleResidue(op); + _sendTokens(op); + + if (op.data.tokenOut != address(0)) { + uint256 balanceAfter = IERC20(op.data.tokenOut).balanceOf(msg.sender); + if (balanceAfter >= results[i].amountOut) { + results[i].amountOut = balanceAfter - results[i].amountOut; + } + } } } } @@ -87,7 +124,7 @@ contract KrMulticall { } } - function _handleResidue(Op calldata _op) internal { + function _sendTokens(Op calldata _op) internal { if (address(this).balance > 0) payable(msg.sender).transfer(address(this).balance); if (_op.data.tokenIn != address(0)) { @@ -101,7 +138,9 @@ contract KrMulticall { if (_op.data.tokenOut != address(0)) { IERC20 tokenOut = IERC20(_op.data.tokenOut); uint256 balance = tokenOut.balanceOf(address(this)); - if (balance != 0) tokenOut.transfer(msg.sender, balance); + if (balance != 0) { + tokenOut.transfer(msg.sender, balance); + } } } @@ -170,7 +209,7 @@ contract KrMulticall { abi.encodePacked( abi.encodeCall( ISCDPSwapFacet.swapSCDP, - (msg.sender, _op.data.tokenIn, _op.data.tokenOut, _op.data.amountIn, _op.data.amountMin) + (msg.sender, _op.data.tokenIn, _op.data.tokenOut, _op.data.amountIn, _op.data.amountOutMin) ), rsPayload ) @@ -205,14 +244,31 @@ contract KrMulticall { _approve(kiss, _op.data.amountIn, kiss); IVaultExtender(kiss).vaultRedeem(_op.data.tokenOut, _op.data.amountIn, msg.sender, msg.sender); return (true, ""); + } else if (_op.action == OpAction.AMMExactInput) { + _approve(address(uniswapRouter), _op.data.amountIn, _op.data.tokenIn); + if ( + uniswapRouter.exactInput( + ISwapRouter.ExactInputParams({ + path: _op.data.path, + recipient: msg.sender, + deadline: _op.data.deadline, + amountIn: _op.data.amountIn, + amountOutMinimum: _op.data.amountOutMin + }) + ) == 0 + ) { + revert ZeroOrInvalidAmountOut( + _op.action, + _op.data.tokenOut, + IERC20(_op.data.tokenOut).symbol(), + IERC20(_op.data.tokenOut).balanceOf(address(this)), + _op.data.amountOutMin + ); + } + return (true, ""); } else { revert InvalidOpAction(_op.action); } - // else if (action == OpAction.AMMIn) { - // revert InvalidOpAction(uint256(action)); - // } else if (action == OpAction.AMMOut) { - // revert InvalidOpAction(uint256(action)); - // } } function _revert(bytes memory data) internal pure { diff --git a/src/contracts/test/Periphery.t.sol b/src/contracts/test/Periphery.t.sol index 3140a46a..99c9d5a7 100644 --- a/src/contracts/test/Periphery.t.sol +++ b/src/contracts/test/Periphery.t.sol @@ -137,45 +137,6 @@ contract PeripheryTest is TestBase("MNEMONIC_DEVNET"), KreskoForgeUtils { kresko.grantRole(Role.MANAGER, address(mc)); } - function testMulticall() public { - assertTrue(address(mc) != address(0), "mc-address"); - KrMulticall.Op[] memory ops = new KrMulticall.Op[](7); - ops[0] = KrMulticall.Op({ - action: KrMulticall.OpAction.MinterBorrow, - data: KrMulticall.OpData(address(0), 0, krJPY.addr, 10000e18, 0, 0) - }); - ops[1] = KrMulticall.Op({ - action: KrMulticall.OpAction.MinterBorrow, - data: KrMulticall.OpData(address(0), 0, krJPY.addr, 10000e18, 0, 0) - }); - ops[2] = KrMulticall.Op({ - action: KrMulticall.OpAction.MinterBorrow, - data: KrMulticall.OpData(address(0), 0, krJPY.addr, 10000e18, 0, 0) - }); - ops[3] = KrMulticall.Op({ - action: KrMulticall.OpAction.MinterDeposit, - data: KrMulticall.OpData(krJPY.addr, 10000e18, address(0), 0, 0, 0) - }); - ops[4] = KrMulticall.Op({ - action: KrMulticall.OpAction.MinterDeposit, - data: KrMulticall.OpData(krJPY.addr, 10000e18, address(0), 0, 0, 0) - }); - ops[5] = KrMulticall.Op({ - action: KrMulticall.OpAction.MinterDeposit, - data: KrMulticall.OpData(krJPY.addr, 10000e18, address(0), 0, 0, 0) - }); - ops[6] = KrMulticall.Op({ - action: KrMulticall.OpAction.MinterDeposit, - data: KrMulticall.OpData(krJPY.addr, 10000e18, address(0), 0, 0, 0) - }); - - address(mc).clg("mc-address"); - - prank(user1); - krJPY.asToken.approve(address(mc), type(uint256).max); - mc.execute(ops, redstoneCallData); - } - function testProtocolDatas() public { // (, bytes memory data) = address(dataV1).staticcall( // abi.encodePacked(abi.encodeWithSelector(dataV1.getGlobalsRs.selector), redstoneCallData) From f0cb6b9118c21e8e1f68be28aecad86e1349bae5 Mon Sep 17 00:00:00 2001 From: panukettu Date: Mon, 20 Nov 2023 14:15:33 +0200 Subject: [PATCH 2/2] misc: move multicall tests to own file --- src/contracts/test/Multicall.t.sol | 333 +++++++++++++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 src/contracts/test/Multicall.t.sol diff --git a/src/contracts/test/Multicall.t.sol b/src/contracts/test/Multicall.t.sol new file mode 100644 index 00000000..4872ab1a --- /dev/null +++ b/src/contracts/test/Multicall.t.sol @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ShortAssert} from "kresko-lib/utils/ShortAssert.sol"; +import {Help, Log} from "kresko-lib/utils/Libs.sol"; +import {Role} from "common/Constants.sol"; +import {Local} from "scripts/deploy/Run.s.sol"; +import {Test} from "forge-std/Test.sol"; +import {state} from "scripts/deploy/base/DeployState.s.sol"; +import {PType} from "periphery/PTypes.sol"; +import {DataV1} from "periphery/DataV1.sol"; +import {IDataFacet} from "periphery/interfaces/IDataFacet.sol"; +import {Errors} from "common/Errors.sol"; +import {VaultAsset} from "vault/VTypes.sol"; +import {PercentageMath} from "libs/PercentageMath.sol"; +import {Asset} from "common/Types.sol"; +import {SCDPAssetIndexes} from "scdp/STypes.sol"; +import {WadRay} from "libs/WadRay.sol"; +import {KrMulticall} from "periphery/KrMulticall.sol"; + +// solhint-disable state-visibility, max-states-count, var-name-mixedcase, no-global-import, const-name-snakecase, no-empty-blocks, no-console + +contract AuditTest is Local { + using ShortAssert for *; + using Help for *; + using Log for *; + using WadRay for *; + using PercentageMath for *; + + bytes redstoneCallData; + DataV1 internal dataV1; + KrMulticall internal mc; + string internal rsPrices; + uint256 constant ETH_PRICE = 2000; + + struct FeeTestRebaseConfig { + uint248 rebaseMultiplier; + bool positive; + uint256 ethPrice; + uint256 firstLiquidationPrice; + uint256 secondLiquidationPrice; + } + + function setUp() public { + rsPrices = initialPrices; + + // enableLogger(); + address deployer = getAddr(0); + address admin = getAddr(0); + address treasury = getAddr(10); + vm.deal(deployer, 100 ether); + + UserCfg[] memory userCfg = super.createUserConfig(testUsers); + AssetsOnChain memory assets = deploy(deployer, admin, treasury); + setupUsers(userCfg, assets); + + dataV1 = new DataV1(IDataFacet(address(kresko)), address(vkiss), address(kiss)); + kiss = state().kiss; + mc = state().multicall; + + prank(getAddr(0)); + redstoneCallData = getRedstonePayload(rsPrices); + mockUSDC.asToken.approve(address(kresko), type(uint256).max); + krETH.asToken.approve(address(kresko), type(uint256).max); + _setETHPrice(ETH_PRICE); + // 1000 KISS -> 0.48 ETH + call(kresko.swapSCDP.selector, getAddr(0), address(state().kiss), krETH.addr, 1000e18, 0, rsPrices); + vkiss.setDepositFee(address(USDT), 10e2); + vkiss.setWithdrawFee(address(USDT), 10e2); + } + + function testMulticall() public { + KrMulticall.Op[] memory ops = new KrMulticall.Op[](9); + ops[0] = KrMulticall.Op({ + action: KrMulticall.OpAction.MinterBorrow, + data: KrMulticall.OpData(address(0), krJPY.addr, 0, 10000e18, 0, 0, 0, "") + }); + ops[1] = KrMulticall.Op({ + action: KrMulticall.OpAction.MinterBorrow, + data: KrMulticall.OpData(address(0), krJPY.addr, 0, 10000e18, 0, 0, 0, "") + }); + ops[2] = KrMulticall.Op({ + action: KrMulticall.OpAction.MinterBorrow, + data: KrMulticall.OpData(address(0), krJPY.addr, 0, 10000e18, 0, 0, 0, "") + }); + ops[3] = KrMulticall.Op({ + action: KrMulticall.OpAction.MinterDeposit, + data: KrMulticall.OpData(krJPY.addr, address(0), 10000e18, 0, 0, 0, 0, "") + }); + ops[4] = KrMulticall.Op({ + action: KrMulticall.OpAction.MinterDeposit, + data: KrMulticall.OpData(krJPY.addr, address(0), 10000e18, 0, 0, 0, 0, "") + }); + ops[5] = KrMulticall.Op({ + action: KrMulticall.OpAction.MinterDeposit, + data: KrMulticall.OpData(krJPY.addr, address(0), 10000e18, 0, 0, 0, 0, "") + }); + ops[6] = KrMulticall.Op({ + action: KrMulticall.OpAction.MinterDeposit, + data: KrMulticall.OpData(krJPY.addr, address(0), 10000e18, 0, 0, 0, 0, "") + }); + ops[7] = KrMulticall.Op({ + action: KrMulticall.OpAction.MinterBorrow, + data: KrMulticall.OpData(address(0), krJPY.addr, 0, 10000e18, 0, 0, 0, "") + }); + ops[8] = KrMulticall.Op({ + action: KrMulticall.OpAction.SCDPTrade, + data: KrMulticall.OpData(krJPY.addr, krETH.addr, 10000e18, 0, 0, 0, 0, "") + }); + + prank(getAddr(0)); + krJPY.asToken.approve(address(mc), type(uint256).max); + KrMulticall.OpResult[] memory results = mc.execute(ops, redstoneCallData); + for (uint256 i; i < results.length; i++) { + results[i].tokenIn.clg("tokenIn"); + results[i].amountIn.clg("amountIn"); + results[i].tokenOut.clg("tokenOut"); + results[i].amountOut.clg("amountOut"); + } + } + + /* -------------------------------- Util -------------------------------- */ + + function _feeTestRebaseConfig(uint248 multiplier, bool positive) internal pure returns (FeeTestRebaseConfig memory) { + if (positive) { + return + FeeTestRebaseConfig({ + positive: positive, + rebaseMultiplier: multiplier * 1e18, + ethPrice: ETH_PRICE / multiplier, + firstLiquidationPrice: 28000 / multiplier, + secondLiquidationPrice: 17500 / multiplier + }); + } + return + FeeTestRebaseConfig({ + positive: positive, + rebaseMultiplier: multiplier * 1e18, + ethPrice: ETH_PRICE * multiplier, + firstLiquidationPrice: 28000 * multiplier, + secondLiquidationPrice: 17500 * multiplier + }); + } + + function _setETHPriceAndSwap(uint256 price, uint256 swapAmount) internal { + prank(getAddr(0)); + _setETHPrice(price); + kresko.setAssetKFactor(krETH.addr, 1.2e4); + call(kresko.swapSCDP.selector, getAddr(0), address(kiss), krETH.addr, swapAmount, 0, rsPrices); + } + + function _tradeSetEthPriceAndLiquidate(uint256 price, uint256 count) internal { + prank(getAddr(0)); + uint256 debt = kresko.getDebtSCDP(krETH.addr); + if (debt < krETH.asToken.balanceOf(getAddr(0))) { + mockUSDC.mock.mint(getAddr(0), 100_000e18); + call(kresko.depositCollateral.selector, getAddr(0), mockUSDC.addr, 100_000e18, rsPrices); + call(kresko.mintKreskoAsset.selector, getAddr(0), krETH.addr, debt, rsPrices); + } + kresko.setAssetKFactor(krETH.addr, 1e4); + for (uint256 i = 0; i < count; i++) { + _setETHPrice(ETH_PRICE); + _trades(1); + prank(getAddr(0)); + _setETHPrice(price); + staticCall(kresko.getCollateralRatioSCDP.selector, rsPrices).pct("CR: before-liq"); + _liquidate(krETH.addr, 1e8, address(kiss)); + } + } + + function _setETHPriceAndLiquidate(uint256 price) internal { + prank(getAddr(0)); + uint256 debt = kresko.getDebtSCDP(krETH.addr); + if (debt < krETH.asToken.balanceOf(getAddr(0))) { + mockUSDC.mock.mint(getAddr(0), 100_000e18); + call(kresko.depositCollateral.selector, getAddr(0), mockUSDC.addr, 100_000e18, rsPrices); + call(kresko.mintKreskoAsset.selector, getAddr(0), krETH.addr, debt, rsPrices); + } + + kresko.setAssetKFactor(krETH.addr, 1e4); + _setETHPrice(price); + staticCall(kresko.getCollateralRatioSCDP.selector, rsPrices).pct("CR: before-liq"); + _liquidate(krETH.addr, debt, address(kiss)); + // staticCall(kresko.getCollateralRatioSCDP.selector, rsPrices).pct("CR: after-liq"); + } + + function _setETHPriceAndLiquidate(uint256 price, uint256 amount) internal { + prank(getAddr(0)); + if (amount < krETH.asToken.balanceOf(getAddr(0))) { + mockUSDC.mock.mint(getAddr(0), 100_000e18); + call(kresko.depositCollateral.selector, getAddr(0), mockUSDC.addr, 100_000e18, rsPrices); + call(kresko.mintKreskoAsset.selector, getAddr(0), krETH.addr, amount, rsPrices); + } + + kresko.setAssetKFactor(krETH.addr, 1e4); + _setETHPrice(price); + staticCall(kresko.getCollateralRatioSCDP.selector, rsPrices).pct("CR: before-liq"); + _liquidate(krETH.addr, amount.wadDiv(price * 1e18), address(kiss)); + // staticCall(kresko.getCollateralRatioSCDP.selector, rsPrices).pct("CR: after-liq"); + } + + function _setETHPriceAndCover(uint256 price, uint256 amount) internal { + prank(getAddr(0)); + uint256 debt = kresko.getDebtSCDP(krETH.addr); + mockUSDC.mock.mint(getAddr(0), 100_000e18); + mockUSDC.asToken.approve(address(kiss), type(uint256).max); + kiss.vaultMint(address(USDC), amount, getAddr(0)); + kiss.approve(address(kresko), type(uint256).max); + + kresko.setAssetKFactor(krETH.addr, 1e4); + _setETHPrice(price); + staticCall(kresko.getCollateralRatioSCDP.selector, rsPrices).pct("CR: before-cover"); + _cover(amount, address(kiss)); + staticCall(kresko.getCollateralRatioSCDP.selector, rsPrices).pct("CR: after-cover"); + } + + function _setETHPriceAndCoverIncentive(uint256 price, uint256 amount) internal { + prank(getAddr(0)); + uint256 debt = kresko.getDebtSCDP(krETH.addr); + mockUSDC.mock.mint(getAddr(0), 100_000e18); + mockUSDC.asToken.approve(address(kiss), type(uint256).max); + kiss.vaultMint(address(USDC), amount, getAddr(0)); + kiss.approve(address(kresko), type(uint256).max); + + kresko.setAssetKFactor(krETH.addr, 1e4); + _setETHPrice(price); + staticCall(kresko.getCollateralRatioSCDP.selector, rsPrices).pct("CR: before-cover"); + _coverIncentive(amount, address(kiss)); + staticCall(kresko.getCollateralRatioSCDP.selector, rsPrices).pct("CR: after-cover"); + } + + function _trades(uint256 count) internal { + address trader = getAddr(777); + prank(deployCfg.admin); + uint256 mintAmount = 20000e18; + + kresko.setFeeAssetSCDP(address(kiss)); + mockUSDC.mock.mint(trader, mintAmount * count); + + prank(trader); + mockUSDC.mock.approve(address(kiss), type(uint256).max); + kiss.approve(address(kresko), type(uint256).max); + krETH.asToken.approve(address(kresko), type(uint256).max); + (uint256 tradeAmount, ) = kiss.vaultDeposit(address(USDC), mintAmount * count, trader); + + for (uint256 i = 0; i < count; i++) { + call(kresko.swapSCDP.selector, trader, address(kiss), krETH.addr, tradeAmount / count, 0, rsPrices); + call(kresko.swapSCDP.selector, trader, krETH.addr, address(kiss), krETH.asToken.balanceOf(trader), 0, rsPrices); + } + } + + function _cover(uint256 _coverAmount, address _seizeAsset) internal returns (uint256 crAfter, uint256 debtValAfter) { + (bool success, bytes memory returndata) = address(kresko).call( + abi.encodePacked(abi.encodeWithSelector(kresko.coverSCDP.selector, address(kiss), _coverAmount), redstoneCallData) + ); + if (!success) _revert(returndata); + return ( + staticCall(kresko.getCollateralRatioSCDP.selector, rsPrices), + staticCall(kresko.getTotalDebtValueSCDP.selector, true, rsPrices) + ); + } + + function _coverIncentive( + uint256 _coverAmount, + address _seizeAsset + ) internal returns (uint256 crAfter, uint256 debtValAfter) { + (bool success, bytes memory returndata) = address(kresko).call( + abi.encodePacked( + abi.encodeWithSelector(kresko.coverWithIncentiveSCDP.selector, address(kiss), _coverAmount, address(kiss)), + redstoneCallData + ) + ); + if (!success) _revert(returndata); + return ( + staticCall(kresko.getCollateralRatioSCDP.selector, rsPrices), + staticCall(kresko.getTotalDebtValueSCDP.selector, true, rsPrices) + ); + } + + function _liquidate( + address _repayAsset, + uint256 _repayAmount, + address _seizeAsset + ) internal returns (uint256 crAfter, uint256 debtValAfter, uint256 debtAmountAfter) { + (bool success, bytes memory returndata) = address(kresko).call( + abi.encodePacked( + abi.encodeWithSelector(kresko.liquidateSCDP.selector, _repayAsset, _repayAmount, _seizeAsset), + redstoneCallData + ) + ); + if (!success) _revert(returndata); + return ( + staticCall(kresko.getCollateralRatioSCDP.selector, rsPrices), + staticCall(kresko.getDebtValueSCDP.selector, _repayAsset, true, rsPrices), + kresko.getDebtSCDP(_repayAsset) + ); + } + + function _previewSwap( + address _assetIn, + address _assetOut, + uint256 _amountIn, + uint256 _minAmountOut + ) internal view returns (uint256 amountOut_) { + (bool success, bytes memory returndata) = address(kresko).staticcall( + abi.encodePacked( + abi.encodeWithSelector(kresko.previewSwapSCDP.selector, _assetIn, _assetOut, _amountIn, _minAmountOut), + redstoneCallData + ) + ); + if (!success) _revert(returndata); + amountOut_ = abi.decode(returndata, (uint256)); + } + + function _setETHPrice(uint256 _pushPrice) internal returns (string memory) { + mockFeedETH.setPrice(_pushPrice * 1e8); + price_eth_rs = ("ETH:").and(_pushPrice.str()).and(":8"); + _updateRsPrices(); + } + + function _getPrice(address _asset) internal view returns (uint256 price_) { + (bool success, bytes memory returndata) = address(kresko).staticcall( + abi.encodePacked(abi.encodeWithSelector(kresko.getPrice.selector, _asset), redstoneCallData) + ); + require(success, "getPrice-failed"); + price_ = abi.decode(returndata, (uint256)); + } + + function _updateRsPrices() internal { + rsPrices = createPriceString(); + redstoneCallData = getRedstonePayload(rsPrices); + } +}