diff --git a/README.md b/README.md index 93b6923..d60af7c 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,7 @@ Tests: Considerations: -- [ ] Consider moving the protocol fee rate to the fee collector contract. - Cleanup: -- [ ] Change `close()` to `reduce()` in `Position.sol` -- [ ] Change all relevant test names related from `close` to `reduce` - [ ] Ensure terminology and variable references are consistent across all comments - [ ] Ensure full NatSpec comliance diff --git a/src/Position.sol b/src/Position.sol index 48f7daa..ce2c784 100644 --- a/src/Position.sol +++ b/src/Position.sol @@ -35,9 +35,9 @@ contract Position is DebtService, SwapService, IPosition { /// @param bAmt The amount of base token received and subsequently supplied as collateral (units: B_DECIMALS). event AddLeverage(uint256 dAmt, uint256 bAmt); - /// @notice An event emitted when a position is closed. + /// @notice An event emitted when a position is reduced. /// @param gains The amount of base token gained from the position (units: B_DECIMALS). - event Close(uint256 gains); + event Reduce(uint256 gains); /// @notice This function is called when a Position contract is deployed. /// @param _owner The account address of the Position contract's owner. @@ -113,7 +113,7 @@ contract Position is DebtService, SwapService, IPosition { } /// @inheritdoc IPosition - function close( + function reduce( uint24 _poolFee, bool _exactOutput, uint256 _swapAmtOutMin, @@ -150,6 +150,6 @@ contract Position is DebtService, SwapService, IPosition { SafeTransferLib.safeTransfer(ERC20(B_TOKEN), OWNER, gains); } - emit Close(gains); + emit Reduce(gains); } } diff --git a/src/interfaces/IPosition.sol b/src/interfaces/IPosition.sol index ba65506..874e3cb 100644 --- a/src/interfaces/IPosition.sol +++ b/src/interfaces/IPosition.sol @@ -69,7 +69,7 @@ interface IPosition is IDebtService { /// @param _swapAmtOutMin The min amount of output tokens from swap (supply 0 if _exactOutput = true). /// @param _withdrawCAmt The amount of C_TOKEN to withdraw (units: C_DECIMALS). /// @param _withdrawBAmt The amount of B_TOKEN to withdraw (units: B_DECIMALS). - function close( + function reduce( uint24 _poolFee, bool _exactOutput, uint256 _swapAmtOutMin, diff --git a/test/Position.t.sol b/test/Position.t.sol index dcff893..3acd528 100644 --- a/test/Position.t.sol +++ b/test/Position.t.sol @@ -197,7 +197,7 @@ contract PositionTest is Test, TokenUtils, DebtUtils { /// @dev // - It should revert with Unauthorized() error when called by an unauthorized sender. - function testFuzz_CannotClose(address _sender) public { + function testFuzz_CannotReduce(address _sender) public { // Assumptions vm.assume(_sender != owner); @@ -224,7 +224,7 @@ contract PositionTest is Test, TokenUtils, DebtUtils { // Act vm.prank(_sender); vm.expectRevert(AdminService.Unauthorized.selector); - IPosition(addr).close(TEST_POOL_FEE, false, 0, cAmt, dAmt); + IPosition(addr).reduce(TEST_POOL_FEE, false, 0, cAmt, dAmt); // Revert to snapshot vm.revertTo(id); diff --git a/test/integration/Position.close.gains.t.sol b/test/integration/Position.close.gains.t.sol index db91c41..5758d2a 100644 --- a/test/integration/Position.close.gains.t.sol +++ b/test/integration/Position.close.gains.t.sol @@ -28,7 +28,7 @@ import { IAaveOracle } from "src/interfaces/aave/IAaveOracle.sol"; import { IPosition } from "src/interfaces/IPosition.sol"; import { IERC20 } from "src/interfaces/token/IERC20.sol"; -contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { +contract PositionReduceGainsTest is Test, TokenUtils, DebtUtils { /* solhint-disable func-name-mixedcase */ struct TestPosition { @@ -120,7 +120,7 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { // - Owner's cToken balance should increase by the amount of collateral withdrawn. // - Owner's bToken balance should increase by the position's gains amount. // - The above should be true for all supported tokens. - function test_CloseExactOutputDiffCAndB() public { + function test_ReduceExactOutputDiffCAndB() public { // Setup ContractBalances memory contractBalances; OwnerBalances memory ownerBalances; @@ -165,7 +165,7 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { /// @dev start event recorder vm.recordLogs(); /// @dev since profitable, withdrawCAmt is max int and withdrawBAmt is its (base) AToken balance - IPosition(p.addr).close(TEST_POOL_FEE, true, 0, type(uint256).max, contractBalances.preBAToken); + IPosition(p.addr).reduce(TEST_POOL_FEE, true, 0, type(uint256).max, contractBalances.preBAToken); VmSafe.Log[] memory entries = vm.getRecordedLogs(); // Get post-act balances @@ -176,11 +176,11 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { ownerBalances.postBToken = IERC20(p.bToken).balanceOf(owner); ownerBalances.postCToken = IERC20(p.cToken).balanceOf(owner); - bytes memory closeEvent = entries[entries.length - 1].data; + bytes memory reduceEvent = entries[entries.length - 1].data; uint256 gains; assembly { - gains := mload(add(closeEvent, 0x20)) + gains := mload(add(reduceEvent, 0x20)) } // Assertions: @@ -209,7 +209,7 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { // - Owner's cToken balance should increase by (collateral withdrawn + position's gains). // - Owner's bToken balance should equal its cToken balance. // - The above should be true for all supported tokens. - function test_CloseExactOutputSameCAndB() public { + function test_ReduceExactOutputSameCAndB() public { // Setup ContractBalances memory contractBalances; OwnerBalances memory ownerBalances; @@ -262,7 +262,7 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { // Act /// @dev since profitable, withdrawCAmt is max int and withdrawBAmt is its (base) AToken balance - IPosition(p.addr).close(TEST_POOL_FEE, true, 0, type(uint256).max, suppliedBAmt); + IPosition(p.addr).reduce(TEST_POOL_FEE, true, 0, type(uint256).max, suppliedBAmt); entries = vm.getRecordedLogs(); // Get post-act balances @@ -273,11 +273,11 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { ownerBalances.postBToken = IERC20(p.bToken).balanceOf(owner); ownerBalances.postCToken = IERC20(p.cToken).balanceOf(owner); - bytes memory closeEvent = entries[entries.length - 1].data; + bytes memory reduceEvent = entries[entries.length - 1].data; uint256 gains; assembly { - gains := mload(add(closeEvent, 0x20)) + gains := mload(add(reduceEvent, 0x20)) } // Assertions: @@ -303,7 +303,7 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { } } - /// @dev: Simulates close where all B_TOKEN is withdrawn and swapped for D_TOKEN, + /// @dev: Simulates reduction where all B_TOKEN is withdrawn and swapped for D_TOKEN, // where D_TOKEN amount is greater than total debt. // - Position contract's (bToken) AToken balance should go to 0 (full withdraw). // - Position contract's (cToken) AToken balance should go to 0 (full withdraw). @@ -314,7 +314,7 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { // - Owner's cToken balance should increase by the amount of collateral withdrawn. // - Owner's bToken balance should stay the same, as gains will be in debt token if exactInput is called. // - The above should be true for all supported tokens. - function testFuzz_CloseFullExactInputDiffCAndB(uint256 _dAmtRemainder) public { + function testFuzz_ReduceFullExactInputDiffCAndB(uint256 _dAmtRemainder) public { // Setup ContractBalances memory contractBalances; OwnerBalances memory ownerBalances; @@ -366,7 +366,7 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { // Act /// @dev start event recorder vm.recordLogs(); - IPosition(p.addr).close(TEST_POOL_FEE, false, 0, type(uint256).max, contractBalances.preBAToken); + IPosition(p.addr).reduce(TEST_POOL_FEE, false, 0, type(uint256).max, contractBalances.preBAToken); VmSafe.Log[] memory entries = vm.getRecordedLogs(); // Get post-act balances @@ -378,11 +378,11 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { ownerBalances.postBToken = IERC20(p.bToken).balanceOf(owner); ownerBalances.postCToken = IERC20(p.cToken).balanceOf(owner); - bytes memory closeEvent = entries[entries.length - 1].data; + bytes memory reduceEvent = entries[entries.length - 1].data; uint256 gains; assembly { - gains := mload(add(closeEvent, 0x20)) + gains := mload(add(reduceEvent, 0x20)) } // Assertions: @@ -401,11 +401,11 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { } } - /// @dev Tests that close function works when the position has gains and collateral token and base token are the same. + /// @dev Tests that reduce function works when the position has gains and collateral token and base token are the same. /// @notice Test strategy: // - 1. Open a position. In doing so, extract the amount of base token added to Aave. // - 2. Mock Uniswap to ensure position gains. - // - 3. Close the position, such that all B_TOKEN is withdrawn and all C_TOKEN is withdrawn. + // - 3. Reduce the position, such that all B_TOKEN is withdrawn and all C_TOKEN is withdrawn. /// @notice Assertions: // - Position contract's (bToken) AToken balance should go to 0 (full withdraw). @@ -416,7 +416,7 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { // - Owner's cToken balance should increase by the amount of collateral withdrawn. // - Owner's bToken balance should increase by the amount of collateral withdrawn. // - The above should be true for all supported tokens. - function testFuzz_CloseExactInputSameCAndB(uint256 _dAmtRemainder) public { + function testFuzz_ReduceExactInputSameCAndB(uint256 _dAmtRemainder) public { // Setup ContractBalances memory contractBalances; OwnerBalances memory ownerBalances; @@ -474,7 +474,7 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { vm.etch(SWAP_ROUTER, code); // Act - IPosition(p.addr).close(TEST_POOL_FEE, false, 0, type(uint256).max, suppliedBAmt); + IPosition(p.addr).reduce(TEST_POOL_FEE, false, 0, type(uint256).max, suppliedBAmt); entries = vm.getRecordedLogs(); // Get post-act balances @@ -486,11 +486,11 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { ownerBalances.postBToken = IERC20(p.bToken).balanceOf(owner); ownerBalances.postCToken = IERC20(p.cToken).balanceOf(owner); - bytes memory closeEvent = entries[entries.length - 1].data; + bytes memory reduceEvent = entries[entries.length - 1].data; uint256 gains; assembly { - gains := mload(add(closeEvent, 0x20)) + gains := mload(add(reduceEvent, 0x20)) } // Assertions: diff --git a/test/integration/Position.close.losses.t.sol b/test/integration/Position.close.losses.t.sol index 4e32694..5a61b16 100644 --- a/test/integration/Position.close.losses.t.sol +++ b/test/integration/Position.close.losses.t.sol @@ -28,7 +28,7 @@ import { IAaveOracle } from "src/interfaces/aave/IAaveOracle.sol"; import { IPosition } from "src/interfaces/IPosition.sol"; import { IERC20 } from "src/interfaces/token/IERC20.sol"; -contract PositionCloseLossesTest is Test, TokenUtils, DebtUtils { +contract PositionReduceLossesTest is Test, TokenUtils, DebtUtils { /* solhint-disable func-name-mixedcase */ struct TestPosition { @@ -116,24 +116,24 @@ contract PositionCloseLossesTest is Test, TokenUtils, DebtUtils { } } - /// @dev Tests that close function works when the position has losses and collateral token and base token are different. + /// @dev Tests that reduce function works when the position has losses and collateral token and base token are different. /// @notice Test strategy: // - 1. Open a position. // - 2. Mock Uniswap to ensure position losses. // - 3. Using screenshot, obtain max withdrawable collateral amount after withdrawing all B_TOKEN and partially repaying debt. - // - 4. Close the position. + // - 4. Reduce the position. /// @notice assertions. // - Position contract's (bToken) AToken balance should go to 0 (full withdraw). // - Position contract's (cToken) AToken balance should decrease by the amount withdrawn. // - Position contract's dToken balance should be 0; no gains, so all was used for swap. // - Position contract's debt on Aave should decrease by repayment (amount received from the swap). - // - Position contract's debt should be greater than 0 after close. + // - Position contract's debt should be greater than 0 after reduction. // - Owner's cToken balance should increase by the amount of collateral withdrawn. // - Owner's bToken balance should not increase; no gains. // - Position contract's gains should be 0. // - The above should be true for all supported tokens. - function test_CloseExactInputDiffCAndB(uint256 withdrawCAmt) public { + function test_ReduceExactInputDiffCAndB(uint256 withdrawCAmt) public { // Setup ContractBalances memory contractBalances; OwnerBalances memory ownerBalances; @@ -193,7 +193,7 @@ contract PositionCloseLossesTest is Test, TokenUtils, DebtUtils { // Act /// @dev start event recorder vm.recordLogs(); - IPosition(p.addr).close(TEST_POOL_FEE, false, 0, withdrawCAmt, contractBalances.preBAToken); + IPosition(p.addr).reduce(TEST_POOL_FEE, false, 0, withdrawCAmt, contractBalances.preBAToken); VmSafe.Log[] memory entries = vm.getRecordedLogs(); // Get post-act balances @@ -205,11 +205,11 @@ contract PositionCloseLossesTest is Test, TokenUtils, DebtUtils { ownerBalances.postBToken = IERC20(p.bToken).balanceOf(owner); ownerBalances.postCToken = IERC20(p.cToken).balanceOf(owner); - bytes memory closeEvent = entries[entries.length - 1].data; + bytes memory reduceEvent = entries[entries.length - 1].data; uint256 gains; assembly { - gains := mload(add(closeEvent, 0x20)) + gains := mload(add(reduceEvent, 0x20)) } // Assertions @@ -229,24 +229,24 @@ contract PositionCloseLossesTest is Test, TokenUtils, DebtUtils { } } - /// @dev Tests that close function works when the position has losses and collateral token and base token are the same. + /// @dev Tests that reduce function works when the position has losses and collateral token and base token are the same. /// @notice Test strategy: // - 1. Open a position. // - 2. Mock Uniswap to ensure position losses. // - 3. Using screenshot, obtain max withdrawable collateral amount after withdrawing all B_TOKEN and partially repaying debt. - // - 4. Close the position. + // - 4. Reduce the position. /// @notice Assertions: // - Position contract's (bToken) AToken balance should decrease by the amount withdrawn. // - Position contract's (cToken) AToken balance should decrease by the amount withdrawn. // - Position contract's dToken balance should be 0; no gains, so all was used for swap. // - Position contract's debt on Aave should decrease by repayment (amount received from the swap). - // - Position contract's debt should be greater than 0 after close. + // - Position contract's debt should be greater than 0 after reduction. // - Position contract's gains should be 0. // - Owner's cToken balance should increase by the amount of collateral withdrawn. // - Owner's bToken balance should increase by the amount of collateral withdrawn. // - The above should be true for all supported tokens. - function test_CloseExactInputSameCAndB(uint256 withdrawCAmt) public { + function test_ReduceExactInputSameCAndB(uint256 withdrawCAmt) public { // Setup ContractBalances memory contractBalances; OwnerBalances memory ownerBalances; @@ -315,7 +315,7 @@ contract PositionCloseLossesTest is Test, TokenUtils, DebtUtils { withdrawCAmt = bound(withdrawCAmt, 1, maxCTokenWithdrawal); // Act - IPosition(p.addr).close(TEST_POOL_FEE, false, 0, withdrawCAmt, suppliedBAmt); + IPosition(p.addr).reduce(TEST_POOL_FEE, false, 0, withdrawCAmt, suppliedBAmt); entries = vm.getRecordedLogs(); // Get post-act balances @@ -327,11 +327,11 @@ contract PositionCloseLossesTest is Test, TokenUtils, DebtUtils { ownerBalances.postBToken = IERC20(p.bToken).balanceOf(owner); ownerBalances.postCToken = IERC20(p.cToken).balanceOf(owner); - bytes memory closeEvent = entries[entries.length - 1].data; + bytes memory reduceEvent = entries[entries.length - 1].data; uint256 gains; assembly { - gains := mload(add(closeEvent, 0x20)) + gains := mload(add(reduceEvent, 0x20)) } // Assertions diff --git a/test/integration/Position.close.partial.sol b/test/integration/Position.close.partial.sol index 2877bf4..4744521 100644 --- a/test/integration/Position.close.partial.sol +++ b/test/integration/Position.close.partial.sol @@ -27,7 +27,7 @@ import { IAaveOracle } from "src/interfaces/aave/IAaveOracle.sol"; import { IPosition } from "src/interfaces/IPosition.sol"; import { IERC20 } from "src/interfaces/token/IERC20.sol"; -contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { +contract PositionReduceGainsTest is Test, TokenUtils, DebtUtils { /* solhint-disable func-name-mixedcase */ struct TestPosition { @@ -119,7 +119,7 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { } } - /// @dev: Simulates close where not all B_TOKEN is withdrawn and swapped for D_TOKEN, + /// @dev: Simulates reduction where not all B_TOKEN is withdrawn and swapped for D_TOKEN, // where D_TOKEN amount is less than total debt. // - Position contract's (bToken) AToken balance should decrease by _withdrawBAmt. // - Position contract's (cToken) AToken balance should decrease by _withdrawCAmt. @@ -131,7 +131,7 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { // - Owner's bToken balance should stay the same (no gains). // - Gains should be 0 because if there are any, they are unrealized. // - The above should be true for all supported tokens. - function testFuzz_ClosePartialExactInputDiffCAndB(uint256 _withdrawBAmt, uint256 _withdrawCAmt) public { + function testFuzz_ReducePartialExactInputDiffCAndB(uint256 _withdrawBAmt, uint256 _withdrawCAmt) public { // Setup ContractBalances memory contractBalances; OwnerBalances memory ownerBalances; @@ -205,7 +205,7 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { // Act /// @dev start event recorder vm.recordLogs(); - IPosition(p.addr).close(TEST_POOL_FEE, false, 0, _withdrawCAmt, _withdrawBAmt); + IPosition(p.addr).reduce(TEST_POOL_FEE, false, 0, _withdrawCAmt, _withdrawBAmt); VmSafe.Log[] memory entries = vm.getRecordedLogs(); // Get post-act balances @@ -217,11 +217,11 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { ownerBalances.postBToken = IERC20(p.bToken).balanceOf(owner); ownerBalances.postCToken = IERC20(p.cToken).balanceOf(owner); - bytes memory closeEvent = entries[entries.length - 1].data; + bytes memory reduceEvent = entries[entries.length - 1].data; uint256 gains; assembly { - gains := mload(add(closeEvent, 0x20)) + gains := mload(add(reduceEvent, 0x20)) } // Assertions: @@ -242,7 +242,7 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { } } - /// @dev: Simulates close where not all B_TOKEN is withdrawn and swapped for D_TOKEN, + /// @dev: Simulates reduction where not all B_TOKEN is withdrawn and swapped for D_TOKEN, // where D_TOKEN amount is less than total debt. // - Position contract's (cToken) AToken balance should decrease by (_withdrawCAmt + _withdrawBAmt). // - Position contract's (bToken) AToken balance should should equal its (cToken) AToken balance. @@ -254,7 +254,7 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { // - Owner's bToken balance should equal owner's cToken balance. // - Gains should be 0 because if there are any, they are unrealized. // - The above should be true for all supported tokens. - function testFuzz_ClosePartialExactInputSameCAndB(uint256 _withdrawBAmt, uint256 _withdrawCAmt) public { + function testFuzz_ReducePartialExactInputSameCAndB(uint256 _withdrawBAmt, uint256 _withdrawCAmt) public { // Setup ContractBalances memory contractBalances; OwnerBalances memory ownerBalances; @@ -321,7 +321,7 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { // Act /// @dev start event recorder vm.recordLogs(); - IPosition(p.addr).close(TEST_POOL_FEE, false, 0, _withdrawCAmt, _withdrawBAmt); + IPosition(p.addr).reduce(TEST_POOL_FEE, false, 0, _withdrawCAmt, _withdrawBAmt); VmSafe.Log[] memory entries = vm.getRecordedLogs(); // Get post-act balances @@ -333,11 +333,11 @@ contract PositionCloseGainsTest is Test, TokenUtils, DebtUtils { ownerBalances.postBToken = IERC20(p.bToken).balanceOf(owner); ownerBalances.postCToken = IERC20(p.cToken).balanceOf(owner); - bytes memory closeEvent = entries[entries.length - 1].data; + bytes memory reduceEvent = entries[entries.length - 1].data; uint256 gains; assembly { - gains := mload(add(closeEvent, 0x20)) + gains := mload(add(reduceEvent, 0x20)) } // Assertions: diff --git a/test/integration/Position.reduce.gains.t.sol b/test/integration/Position.reduce.gains.t.sol new file mode 100644 index 0000000..5758d2a --- /dev/null +++ b/test/integration/Position.reduce.gains.t.sol @@ -0,0 +1,515 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +// External Imports +import { Test } from "forge-std/Test.sol"; +import { VmSafe } from "forge-std/Vm.sol"; + +// Local Imports +import { PositionFactory } from "src/PositionFactory.sol"; +import { + Assets, + AAVE_ORACLE, + CONTRACT_DEPLOYER, + DAI, + FEE_COLLECTOR, + PROFIT_PERCENT, + PROTOCOL_FEE_RATE, + SWAP_ROUTER, + TEST_CLIENT, + TEST_LTV, + TEST_POOL_FEE, + USDC +} from "test/common/Constants.t.sol"; +import { TokenUtils } from "test/common/utils/TokenUtils.t.sol"; +import { DebtUtils } from "test/common/utils/DebtUtils.t.sol"; +import { MockUniswapGains } from "test/mocks/MockUniswap.t.sol"; +import { IAaveOracle } from "src/interfaces/aave/IAaveOracle.sol"; +import { IPosition } from "src/interfaces/IPosition.sol"; +import { IERC20 } from "src/interfaces/token/IERC20.sol"; + +contract PositionReduceGainsTest is Test, TokenUtils, DebtUtils { + /* solhint-disable func-name-mixedcase */ + + struct TestPosition { + address addr; + address cToken; + address dToken; + address bToken; + } + + struct ContractBalances { + uint256 preBToken; + uint256 postBToken; + uint256 preVDToken; + uint256 postVDToken; + uint256 preCAToken; + uint256 postCAToken; + uint256 preBAToken; + uint256 postBAToken; + uint256 preDToken; + uint256 postDToken; + } + + struct OwnerBalances { + uint256 preBToken; + uint256 postBToken; + uint256 preCToken; + uint256 postCToken; + } + + // Test contracts + PositionFactory public positionFactory; + Assets public assets; + TestPosition[] public positions; + + // Test Storage + address public positionAddr; + address public owner = address(this); + + function setUp() public { + // Deploy assets + assets = new Assets(); + address[4] memory supportedAssets = assets.getSupported(); + + // Deploy FeeCollector + vm.prank(CONTRACT_DEPLOYER); + deployCodeTo("FeeCollector.sol", abi.encode(CONTRACT_DEPLOYER, PROTOCOL_FEE_RATE), FEE_COLLECTOR); + + // Deploy PositionFactory + vm.prank(CONTRACT_DEPLOYER); + positionFactory = new PositionFactory(CONTRACT_DEPLOYER); + + // Deploy and store all possible positions + for (uint256 i; i < supportedAssets.length; i++) { + address cToken = supportedAssets[i]; + for (uint256 j; j < supportedAssets.length; j++) { + if (j != i) { + address dToken = supportedAssets[j]; + for (uint256 k; k < supportedAssets.length; k++) { + address bToken = supportedAssets[k]; + // Exclude positions with no pool + bool poolExists = !((dToken == USDC && bToken == DAI) || (dToken == DAI && bToken == USDC)); + if (k != j && poolExists) { + positionAddr = positionFactory.createPosition(cToken, dToken, bToken); + TestPosition memory newPosition = + TestPosition({ addr: positionAddr, cToken: cToken, dToken: dToken, bToken: bToken }); + positions.push(newPosition); + } + } + } + } + } + + // Mock AaveOracle + for (uint256 i; i < supportedAssets.length; i++) { + vm.mockCall( + AAVE_ORACLE, + abi.encodeWithSelector(IAaveOracle(AAVE_ORACLE).getAssetPrice.selector, supportedAssets[i]), + abi.encode(assets.prices(supportedAssets[i])) + ); + } + } + + /// @dev + // - Position contract's (bToken) AToken balance should go to 0 (full withdraw). + // - Position contract's (cToken) AToken balance should go to 0 (full withdraw). + // - Position contract's bToken balance should remain 0. + // - Position contract's debt on Aave should go to 0. + // - Position gains should be equal to the supplied base token amount times profit percent (uniswap mock takes 1 - PROFIT_PERCENT of input tokens). + // - Owner's cToken balance should increase by the amount of collateral withdrawn. + // - Owner's bToken balance should increase by the position's gains amount. + // - The above should be true for all supported tokens. + function test_ReduceExactOutputDiffCAndB() public { + // Setup + ContractBalances memory contractBalances; + OwnerBalances memory ownerBalances; + TestPosition memory p; + + // Take snapshot + uint256 id = vm.snapshot(); + for (uint256 i; i < positions.length; i++) { + // Test variables + p.addr = positions[i].addr; + p.cToken = positions[i].cToken; + p.dToken = positions[i].dToken; + p.bToken = positions[i].bToken; + + if (p.cToken != p.bToken) { + // Setup: open position + _fund(owner, p.cToken, assets.maxCAmts(p.cToken)); + IERC20(p.cToken).approve(p.addr, assets.maxCAmts(p.cToken)); + IPosition(p.addr).add(assets.maxCAmts(p.cToken), TEST_LTV, 0, TEST_POOL_FEE, TEST_CLIENT); + + // Get pre-act balances + contractBalances.preBToken = IERC20(p.bToken).balanceOf(p.addr); + contractBalances.preVDToken = _getVariableDebtTokenBalance(p.addr, p.dToken); + contractBalances.preCAToken = _getATokenBalance(p.addr, p.cToken); + contractBalances.preBAToken = _getATokenBalance(p.addr, p.bToken); + ownerBalances.preBToken = IERC20(p.bToken).balanceOf(owner); + ownerBalances.preCToken = IERC20(p.cToken).balanceOf(owner); + + // Assertions + assertEq(ownerBalances.preBToken, 0); + assertEq(contractBalances.preBToken, 0); + assertNotEq(contractBalances.preVDToken, 0); + assertNotEq(contractBalances.preCAToken, 0); + assertNotEq(contractBalances.preBAToken, 0); + + // Mock Uniswap to ensure position gains + _fund(SWAP_ROUTER, p.dToken, contractBalances.preVDToken); + bytes memory code = address(new MockUniswapGains()).code; + vm.etch(SWAP_ROUTER, code); + + // Act + /// @dev start event recorder + vm.recordLogs(); + /// @dev since profitable, withdrawCAmt is max int and withdrawBAmt is its (base) AToken balance + IPosition(p.addr).reduce(TEST_POOL_FEE, true, 0, type(uint256).max, contractBalances.preBAToken); + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + + // Get post-act balances + contractBalances.postBToken = IERC20(p.bToken).balanceOf(p.addr); + contractBalances.postVDToken = _getVariableDebtTokenBalance(p.addr, p.dToken); + contractBalances.postCAToken = _getATokenBalance(p.addr, p.cToken); + contractBalances.postBAToken = _getATokenBalance(p.addr, p.bToken); + ownerBalances.postBToken = IERC20(p.bToken).balanceOf(owner); + ownerBalances.postCToken = IERC20(p.cToken).balanceOf(owner); + + bytes memory reduceEvent = entries[entries.length - 1].data; + uint256 gains; + + assembly { + gains := mload(add(reduceEvent, 0x20)) + } + + // Assertions: + assertEq(contractBalances.postBToken, 0); + assertEq(contractBalances.postVDToken, 0); + assertEq(contractBalances.postCAToken, 0); + assertEq(contractBalances.postBAToken, 0); + /// @dev Uniswap mock takes (1 - PROFIT_PERCENT)% of the input token balance + // at the time it's called, leaving PROFIT_PERCENT on the contract. + assertApproxEqAbs(gains, contractBalances.preBAToken * PROFIT_PERCENT / 100, 1); + assertEq(ownerBalances.postBToken, ownerBalances.preBToken + gains); + assertEq(ownerBalances.postCToken, ownerBalances.preCToken + contractBalances.preCAToken); + + // Revert to snapshot + vm.revertTo(id); + } + } + } + + /// @dev + // - Position contract's (cToken) AToken balance should go to 0 (full withdraw). + // - Position contract's (bToken) AToken balance should equal its (cToken) AToken balance. + // - Position contract's bToken balance should remain 0. + // - Position contract's debt on Aave should go to 0. + // - Position gains should be equal to the supplied base token amount times profit percent (uniswap mock takes 1 - PROFIT_PERCENT of input tokens). + // - Owner's cToken balance should increase by (collateral withdrawn + position's gains). + // - Owner's bToken balance should equal its cToken balance. + // - The above should be true for all supported tokens. + function test_ReduceExactOutputSameCAndB() public { + // Setup + ContractBalances memory contractBalances; + OwnerBalances memory ownerBalances; + TestPosition memory p; + + // Take snapshot + uint256 id = vm.snapshot(); + for (uint256 i; i < positions.length; i++) { + // Test variables + p.addr = positions[i].addr; + p.cToken = positions[i].cToken; + p.dToken = positions[i].dToken; + p.bToken = positions[i].bToken; + + if (p.cToken == p.bToken) { + /// @dev start event recorder + vm.recordLogs(); + // Setup: open position + _fund(owner, p.cToken, assets.maxCAmts(p.cToken)); + IERC20(p.cToken).approve(p.addr, assets.maxCAmts(p.cToken)); + IPosition(p.addr).add(assets.maxCAmts(p.cToken), TEST_LTV, 0, TEST_POOL_FEE, TEST_CLIENT); + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + + // Extract amount of base token added to Aave + bytes memory addEvent = entries[entries.length - 1].data; + uint256 suppliedBAmt; + assembly { + suppliedBAmt := mload(add(addEvent, 0x60)) + } + + // Get pre-act balances + contractBalances.preBToken = IERC20(p.bToken).balanceOf(p.addr); + contractBalances.preVDToken = _getVariableDebtTokenBalance(p.addr, p.dToken); + contractBalances.preCAToken = _getATokenBalance(p.addr, p.cToken); + contractBalances.preBAToken = _getATokenBalance(p.addr, p.bToken); + ownerBalances.preBToken = IERC20(p.bToken).balanceOf(owner); + ownerBalances.preCToken = IERC20(p.cToken).balanceOf(owner); + + // Assertions + assertEq(ownerBalances.preBToken, 0); + assertEq(contractBalances.preBToken, 0); + assertNotEq(contractBalances.preVDToken, 0); + assertNotEq(contractBalances.preCAToken, 0); + assertEq(contractBalances.preBAToken, contractBalances.preCAToken); + + // Mock Uniswap to ensure position gains + _fund(SWAP_ROUTER, p.dToken, contractBalances.preVDToken); + bytes memory code = address(new MockUniswapGains()).code; + vm.etch(SWAP_ROUTER, code); + + // Act + /// @dev since profitable, withdrawCAmt is max int and withdrawBAmt is its (base) AToken balance + IPosition(p.addr).reduce(TEST_POOL_FEE, true, 0, type(uint256).max, suppliedBAmt); + entries = vm.getRecordedLogs(); + + // Get post-act balances + contractBalances.postBToken = IERC20(p.bToken).balanceOf(p.addr); + contractBalances.postVDToken = _getVariableDebtTokenBalance(p.addr, p.dToken); + contractBalances.postCAToken = _getATokenBalance(p.addr, p.cToken); + contractBalances.postBAToken = _getATokenBalance(p.addr, p.bToken); + ownerBalances.postBToken = IERC20(p.bToken).balanceOf(owner); + ownerBalances.postCToken = IERC20(p.cToken).balanceOf(owner); + + bytes memory reduceEvent = entries[entries.length - 1].data; + uint256 gains; + + assembly { + gains := mload(add(reduceEvent, 0x20)) + } + + // Assertions: + assertEq(contractBalances.postBToken, 0); + assertEq(contractBalances.postVDToken, 0); + assertEq(contractBalances.postCAToken, 0); + assertEq(contractBalances.postBAToken, contractBalances.postCAToken); + /// @dev Uniswap mock takes (1 - PROFIT_PERCENT)% of the input token balance + // at the time it's called, leaving PROFIT_PERCENT on the contract. + assertApproxEqAbs(gains, suppliedBAmt * PROFIT_PERCENT / 100, 1); + /// @dev In this case, bToken and cToken balances will increase by the + // same amount (gains + collateral withdrawn - suppliedBAmt). + assertApproxEqAbs( + ownerBalances.postBToken, + ownerBalances.preBToken + gains + (contractBalances.preCAToken - suppliedBAmt), + 1 + ); + assertEq(ownerBalances.postCToken, ownerBalances.postBToken); + + // Revert to snapshot + vm.revertTo(id); + } + } + } + + /// @dev: Simulates reduction where all B_TOKEN is withdrawn and swapped for D_TOKEN, + // where D_TOKEN amount is greater than total debt. + // - Position contract's (bToken) AToken balance should go to 0 (full withdraw). + // - Position contract's (cToken) AToken balance should go to 0 (full withdraw). + /// @notice If B_TOKEN withdraw value > debt value, there will be left over D_TOKEN on the position contract. + // - Position contract's dToken balance should be (swap dAmtOut - debt repayment). + // - Position contract's bToken balance should remain 0. + // - Position contract's debt on Aave should go to 0. + // - Owner's cToken balance should increase by the amount of collateral withdrawn. + // - Owner's bToken balance should stay the same, as gains will be in debt token if exactInput is called. + // - The above should be true for all supported tokens. + function testFuzz_ReduceFullExactInputDiffCAndB(uint256 _dAmtRemainder) public { + // Setup + ContractBalances memory contractBalances; + OwnerBalances memory ownerBalances; + TestPosition memory p; + + // Take snapshot + uint256 id = vm.snapshot(); + for (uint256 i; i < positions.length; i++) { + // Test variables + p.addr = positions[i].addr; + p.cToken = positions[i].cToken; + p.dToken = positions[i].dToken; + p.bToken = positions[i].bToken; + + if (p.cToken != p.bToken) { + // Setup: open position + uint256 cAmt = assets.maxCAmts(p.cToken); + _fund(owner, p.cToken, cAmt); + IERC20(p.cToken).approve(p.addr, cAmt); + IPosition(p.addr).add(cAmt, TEST_LTV, 0, TEST_POOL_FEE, TEST_CLIENT); + + // Get pre-act balances + contractBalances.preBToken = IERC20(p.bToken).balanceOf(p.addr); + contractBalances.preDToken = IERC20(p.dToken).balanceOf(p.addr); + contractBalances.preVDToken = _getVariableDebtTokenBalance(p.addr, p.dToken); + contractBalances.preCAToken = _getATokenBalance(p.addr, p.cToken); + contractBalances.preBAToken = _getATokenBalance(p.addr, p.bToken); + ownerBalances.preBToken = IERC20(p.bToken).balanceOf(owner); + ownerBalances.preCToken = IERC20(p.cToken).balanceOf(owner); + + // Assertions + assertEq(ownerBalances.preBToken, 0); + assertEq(contractBalances.preDToken, 0); + assertEq(contractBalances.preBToken, 0); + assertNotEq(contractBalances.preVDToken, 0); + assertNotEq(contractBalances.preCAToken, 0); + assertNotEq(contractBalances.preBAToken, 0); + + // Bound: upper bound is 150% of preVDToken + uint256 upperBound = contractBalances.preVDToken + (contractBalances.preVDToken * 50) / 100; + _dAmtRemainder = bound(_dAmtRemainder, 2, upperBound); + + // Mock Uniswap to ensure position gains + uint256 amountOut = contractBalances.preVDToken + _dAmtRemainder; + _fund(SWAP_ROUTER, p.dToken, amountOut); + bytes memory code = address(new MockUniswapGains()).code; + vm.etch(SWAP_ROUTER, code); + + // Act + /// @dev start event recorder + vm.recordLogs(); + IPosition(p.addr).reduce(TEST_POOL_FEE, false, 0, type(uint256).max, contractBalances.preBAToken); + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + + // Get post-act balances + contractBalances.postBToken = IERC20(p.bToken).balanceOf(p.addr); + contractBalances.postVDToken = _getVariableDebtTokenBalance(p.addr, p.dToken); + contractBalances.postCAToken = _getATokenBalance(p.addr, p.cToken); + contractBalances.postBAToken = _getATokenBalance(p.addr, p.bToken); + contractBalances.postDToken = IERC20(p.dToken).balanceOf(p.addr); + ownerBalances.postBToken = IERC20(p.bToken).balanceOf(owner); + ownerBalances.postCToken = IERC20(p.cToken).balanceOf(owner); + + bytes memory reduceEvent = entries[entries.length - 1].data; + uint256 gains; + + assembly { + gains := mload(add(reduceEvent, 0x20)) + } + + // Assertions: + assertApproxEqAbs(contractBalances.postDToken, amountOut - contractBalances.preVDToken, 1); + assertEq(contractBalances.postBToken, 0); + assertEq(contractBalances.postVDToken, 0); + assertEq(contractBalances.postCAToken, 0); + assertEq(contractBalances.postBAToken, 0); + assertEq(gains, 0); + assertEq(ownerBalances.postBToken, ownerBalances.preBToken); + assertEq(ownerBalances.postCToken, ownerBalances.preCToken + contractBalances.preCAToken); + + // Revert to snapshot + vm.revertTo(id); + } + } + } + + /// @dev Tests that reduce function works when the position has gains and collateral token and base token are the same. + /// @notice Test strategy: + // - 1. Open a position. In doing so, extract the amount of base token added to Aave. + // - 2. Mock Uniswap to ensure position gains. + // - 3. Reduce the position, such that all B_TOKEN is withdrawn and all C_TOKEN is withdrawn. + + /// @notice Assertions: + // - Position contract's (bToken) AToken balance should go to 0 (full withdraw). + // - Position contract's (cToken) AToken balance should go to 0 (full withdraw). + // - Position contract's dToken balance should be the amount received from swap minus the amount repaid to Aave. + // - Position contract's bToken balance should remain 0. + // - Position contract's debt on Aave should go to 0. + // - Owner's cToken balance should increase by the amount of collateral withdrawn. + // - Owner's bToken balance should increase by the amount of collateral withdrawn. + // - The above should be true for all supported tokens. + function testFuzz_ReduceExactInputSameCAndB(uint256 _dAmtRemainder) public { + // Setup + ContractBalances memory contractBalances; + OwnerBalances memory ownerBalances; + TestPosition memory p; + + // Take snapshot + uint256 id = vm.snapshot(); + for (uint256 i; i < positions.length; i++) { + // Test variables + p.addr = positions[i].addr; + p.cToken = positions[i].cToken; + p.dToken = positions[i].dToken; + p.bToken = positions[i].bToken; + + if (p.cToken == p.bToken) { + /// @dev start event recorder + vm.recordLogs(); + // Setup: open position + _fund(owner, p.cToken, assets.maxCAmts(p.cToken)); + IERC20(p.cToken).approve(p.addr, assets.maxCAmts(p.cToken)); + IPosition(p.addr).add(assets.maxCAmts(p.cToken), TEST_LTV, 0, TEST_POOL_FEE, TEST_CLIENT); + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + + // Extract amount of base token added to Aave + bytes memory addEvent = entries[entries.length - 1].data; + uint256 suppliedBAmt; + assembly { + suppliedBAmt := mload(add(addEvent, 0x60)) + } + + // Get pre-act balances + contractBalances.preBToken = IERC20(p.bToken).balanceOf(p.addr); + contractBalances.preDToken = IERC20(p.dToken).balanceOf(p.addr); + contractBalances.preVDToken = _getVariableDebtTokenBalance(p.addr, p.dToken); + contractBalances.preCAToken = _getATokenBalance(p.addr, p.cToken); + contractBalances.preBAToken = _getATokenBalance(p.addr, p.bToken); + ownerBalances.preBToken = IERC20(p.bToken).balanceOf(owner); + ownerBalances.preCToken = IERC20(p.cToken).balanceOf(owner); + + // Assertions + assertEq(ownerBalances.preBToken, 0); + assertEq(contractBalances.preBToken, 0); + assertNotEq(contractBalances.preVDToken, 0); + assertNotEq(contractBalances.preCAToken, 0); + assertEq(contractBalances.preBAToken, contractBalances.preCAToken); + + // Bound: upper bound is 150% of preVDToken + uint256 upperBound = contractBalances.preVDToken + (contractBalances.preVDToken * 50) / 100; + _dAmtRemainder = bound(_dAmtRemainder, 2, upperBound); + + // Mock Uniswap to ensure position gains + uint256 amountOut = contractBalances.preVDToken + _dAmtRemainder; + _fund(SWAP_ROUTER, p.dToken, contractBalances.preVDToken + _dAmtRemainder); + bytes memory code = address(new MockUniswapGains()).code; + vm.etch(SWAP_ROUTER, code); + + // Act + IPosition(p.addr).reduce(TEST_POOL_FEE, false, 0, type(uint256).max, suppliedBAmt); + entries = vm.getRecordedLogs(); + + // Get post-act balances + contractBalances.postBToken = IERC20(p.bToken).balanceOf(p.addr); + contractBalances.postVDToken = _getVariableDebtTokenBalance(p.addr, p.dToken); + contractBalances.postCAToken = _getATokenBalance(p.addr, p.cToken); + contractBalances.postBAToken = _getATokenBalance(p.addr, p.bToken); + contractBalances.postDToken = IERC20(p.dToken).balanceOf(p.addr); + ownerBalances.postBToken = IERC20(p.bToken).balanceOf(owner); + ownerBalances.postCToken = IERC20(p.cToken).balanceOf(owner); + + bytes memory reduceEvent = entries[entries.length - 1].data; + uint256 gains; + + assembly { + gains := mload(add(reduceEvent, 0x20)) + } + + // Assertions: + assertApproxEqAbs(contractBalances.postDToken, amountOut - contractBalances.preVDToken, 1); + assertEq(contractBalances.postBToken, 0); + assertEq(contractBalances.postVDToken, 0); + assertEq(contractBalances.postCAToken, 0); + assertEq(contractBalances.postBAToken, 0); + assertEq(gains, 0); + /// @dev In this case, bToken and cToken balances will increase by the same amount (collateral withdrawn - suppliedBAmt) + // Gains will be in debt token if exactInput is called. + assertApproxEqAbs( + ownerBalances.postBToken, ownerBalances.preBToken + (contractBalances.preCAToken - suppliedBAmt), 1 + ); + assertEq(ownerBalances.postCToken, ownerBalances.postBToken); + + // Revert to snapshot + vm.revertTo(id); + } + } + } +} diff --git a/test/integration/Position.reduce.losses.t.sol b/test/integration/Position.reduce.losses.t.sol new file mode 100644 index 0000000..46c4c83 --- /dev/null +++ b/test/integration/Position.reduce.losses.t.sol @@ -0,0 +1,356 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +// External Imports +import { Test } from "forge-std/Test.sol"; +import { VmSafe } from "forge-std/Vm.sol"; + +// Local Imports +import { PositionFactory } from "src/PositionFactory.sol"; +import { + Assets, + AAVE_ORACLE, + CONTRACT_DEPLOYER, + DAI, + FEE_COLLECTOR, + PROTOCOL_FEE_RATE, + REPAY_PERCENT, + SWAP_ROUTER, + TEST_CLIENT, + TEST_LTV, + TEST_POOL_FEE, + USDC +} from "test/common/Constants.t.sol"; +import { TokenUtils } from "test/common/utils/TokenUtils.t.sol"; +import { DebtUtils } from "test/common/utils/DebtUtils.t.sol"; +import { MockUniswapLosses } from "test/mocks/MockUniswap.t.sol"; +import { IAaveOracle } from "src/interfaces/aave/IAaveOracle.sol"; +import { IPosition } from "src/interfaces/IPosition.sol"; +import { IERC20 } from "src/interfaces/token/IERC20.sol"; + +contract PositionReduceLossesTest is Test, TokenUtils, DebtUtils { + /* solhint-disable func-name-mixedcase */ + + struct TestPosition { + address addr; + address cToken; + address dToken; + address bToken; + } + + struct ContractBalances { + uint256 preBToken; + uint256 postBToken; + uint256 preVDToken; + uint256 postVDToken; + uint256 preCAToken; + uint256 postCAToken; + uint256 preBAToken; + uint256 postBAToken; + uint256 preDToken; + uint256 postDToken; + } + + struct OwnerBalances { + uint256 preBToken; + uint256 postBToken; + uint256 preCToken; + uint256 postCToken; + } + + struct SnapShots { + uint256 id1; + uint256 id2; + } + + // Test contracts + PositionFactory public positionFactory; + Assets public assets; + TestPosition[] public positions; + + // Test Storage + address public positionAddr; + address public owner = address(this); + + function setUp() public { + // Deploy assets + assets = new Assets(); + address[4] memory supportedAssets = assets.getSupported(); + + // Deploy FeeCollector + vm.prank(CONTRACT_DEPLOYER); + deployCodeTo("FeeCollector.sol", abi.encode(CONTRACT_DEPLOYER, PROTOCOL_FEE_RATE), FEE_COLLECTOR); + + // Deploy PositionFactory + vm.prank(CONTRACT_DEPLOYER); + positionFactory = new PositionFactory(CONTRACT_DEPLOYER); + + // Deploy and store all possible positions + for (uint256 i; i < supportedAssets.length; i++) { + address cToken = supportedAssets[i]; + for (uint256 j; j < supportedAssets.length; j++) { + if (j != i) { + address dToken = supportedAssets[j]; + for (uint256 k; k < supportedAssets.length; k++) { + address bToken = supportedAssets[k]; + // Exclude positions with no pool + bool poolExists = !((dToken == USDC && bToken == DAI) || (dToken == DAI && bToken == USDC)); + if (k != j && poolExists) { + positionAddr = positionFactory.createPosition(cToken, dToken, bToken); + TestPosition memory newPosition = + TestPosition({ addr: positionAddr, cToken: cToken, dToken: dToken, bToken: bToken }); + positions.push(newPosition); + } + } + } + } + } + + // Mock AaveOracle + for (uint256 i; i < supportedAssets.length; i++) { + vm.mockCall( + AAVE_ORACLE, + abi.encodeWithSelector(IAaveOracle(AAVE_ORACLE).getAssetPrice.selector, supportedAssets[i]), + abi.encode(assets.prices(supportedAssets[i])) + ); + } + } + + /// @dev Tests that reduce function works when the position has losses and collateral token and base token are different. + /// @notice Test strategy: + // - 1. Open a position. + // - 2. Mock Uniswap to ensure position losses. + // - 3. Using screenshot, obtain max withdrawable collateral amount after withdrawing all B_TOKEN and partially repaying debt. + // - 4. Reduce the position. + + /// @notice assertions. + // - Position contract's (bToken) AToken balance should go to 0 (full withdraw). + // - Position contract's (cToken) AToken balance should decrease by the amount withdrawn. + // - Position contract's dToken balance should be 0; no gains, so all was used for swap. + // - Position contract's debt on Aave should decrease by repayment (amount received from the swap). + // - Position contract's debt should be greater than 0 after reduce. + // - Owner's cToken balance should increase by the amount of collateral withdrawn. + // - Owner's bToken balance should not increase; no gains. + // - Position contract's gains should be 0. + // - The above should be true for all supported tokens. + function test_ReduceExactInputDiffCAndB(uint256 withdrawCAmt) public { + // Setup + ContractBalances memory contractBalances; + OwnerBalances memory ownerBalances; + TestPosition memory p; + + // Take snapshot + uint256 id = vm.snapshot(); + + for (uint256 i; i < positions.length; i++) { + // Test variables + p.addr = positions[i].addr; + p.cToken = positions[i].cToken; + p.dToken = positions[i].dToken; + p.bToken = positions[i].bToken; + + if (p.cToken != p.bToken) { + // Setup: open position + _fund(owner, p.cToken, assets.maxCAmts(p.cToken)); + IERC20(p.cToken).approve(p.addr, assets.maxCAmts(p.cToken)); + IPosition(p.addr).add(assets.maxCAmts(p.cToken), TEST_LTV, 0, TEST_POOL_FEE, TEST_CLIENT); + + // Get pre-act balances + contractBalances.preBToken = IERC20(p.bToken).balanceOf(p.addr); + contractBalances.preDToken = IERC20(p.dToken).balanceOf(p.addr); + contractBalances.preVDToken = _getVariableDebtTokenBalance(p.addr, p.dToken); + contractBalances.preCAToken = _getATokenBalance(p.addr, p.cToken); + contractBalances.preBAToken = _getATokenBalance(p.addr, p.bToken); + ownerBalances.preBToken = IERC20(p.bToken).balanceOf(owner); + ownerBalances.preCToken = IERC20(p.cToken).balanceOf(owner); + + // Assertions + assertEq(ownerBalances.preBToken, 0); + assertEq(contractBalances.preDToken, 0); + assertEq(contractBalances.preBToken, 0); + assertNotEq(contractBalances.preVDToken, 0); + assertNotEq(contractBalances.preCAToken, 0); + assertNotEq(contractBalances.preBAToken, 0); + + // Mock Uniswap to ensure position losses + uint256 dAmtOut = contractBalances.preVDToken * REPAY_PERCENT / 100; + _fund(SWAP_ROUTER, p.dToken, dAmtOut); + bytes memory code = address(new MockUniswapLosses()).code; + vm.etch(SWAP_ROUTER, code); + + // Withdraw B_TOKEN and make repayment manually to get exact withdrawCAmt + uint256 repayID = vm.snapshot(); + IPosition(p.addr).withdraw(p.bToken, contractBalances.preBAToken, owner); + _fund(owner, p.dToken, dAmtOut); + IERC20(p.dToken).approve(p.addr, dAmtOut); + IPosition(p.addr).repay(dAmtOut); + uint256 maxCTokenWithdrawal = _getMaxWithdrawAmt(p.addr, p.cToken, assets.decimals(p.cToken)); + vm.revertTo(repayID); + + // Bound fuzzed variables + withdrawCAmt = bound(withdrawCAmt, 1, maxCTokenWithdrawal); + + // Act + /// @dev start event recorder + vm.recordLogs(); + IPosition(p.addr).reduce(TEST_POOL_FEE, false, 0, withdrawCAmt, contractBalances.preBAToken); + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + + // Get post-act balances + contractBalances.postBToken = IERC20(p.bToken).balanceOf(p.addr); + contractBalances.postDToken = IERC20(p.dToken).balanceOf(p.addr); + contractBalances.postVDToken = _getVariableDebtTokenBalance(p.addr, p.dToken); + contractBalances.postCAToken = _getATokenBalance(p.addr, p.cToken); + contractBalances.postBAToken = _getATokenBalance(p.addr, p.bToken); + ownerBalances.postBToken = IERC20(p.bToken).balanceOf(owner); + ownerBalances.postCToken = IERC20(p.cToken).balanceOf(owner); + + bytes memory reduceEvent = entries[entries.length - 1].data; + uint256 gains; + + assembly { + gains := mload(add(reduceEvent, 0x20)) + } + + // Assertions + assertApproxEqAbs(contractBalances.postBAToken, 0, 1); + assertApproxEqAbs(contractBalances.postCAToken, contractBalances.preCAToken - withdrawCAmt, 1); + assertApproxEqAbs(contractBalances.postVDToken, contractBalances.preVDToken - dAmtOut, 1); + assertGt(contractBalances.postVDToken, 0); + assertEq(contractBalances.postDToken, 0); + uint256 withdrawAmt = contractBalances.preCAToken - contractBalances.postCAToken; + assertApproxEqAbs(ownerBalances.postCToken, ownerBalances.preCToken + withdrawAmt, 1); + assertEq(ownerBalances.postBToken, ownerBalances.preBToken); + assertEq(gains, 0); + + // Revert to snapshot + vm.revertTo(id); + } + } + } + + /// @dev Tests that reduce function works when the position has losses and collateral token and base token are the same. + /// @notice Test strategy: + // - 1. Open a position. + // - 2. Mock Uniswap to ensure position losses. + // - 3. Using screenshot, obtain max withdrawable collateral amount after withdrawing all B_TOKEN and partially repaying debt. + // - 4. Reduce the position. + + /// @notice Assertions: + // - Position contract's (bToken) AToken balance should decrease by the amount withdrawn. + // - Position contract's (cToken) AToken balance should decrease by the amount withdrawn. + // - Position contract's dToken balance should be 0; no gains, so all was used for swap. + // - Position contract's debt on Aave should decrease by repayment (amount received from the swap). + // - Position contract's debt should be greater than 0 after reduce. + // - Position contract's gains should be 0. + // - Owner's cToken balance should increase by the amount of collateral withdrawn. + // - Owner's bToken balance should increase by the amount of collateral withdrawn. + // - The above should be true for all supported tokens. + function test_ReduceExactInputSameCAndB(uint256 withdrawCAmt) public { + // Setup + ContractBalances memory contractBalances; + OwnerBalances memory ownerBalances; + TestPosition memory p; + SnapShots memory snapshots; + + // Take snapshot + snapshots.id1 = vm.snapshot(); + + for (uint256 i; i < positions.length; i++) { + // Test variables + p.addr = positions[i].addr; + p.cToken = positions[i].cToken; + p.dToken = positions[i].dToken; + p.bToken = positions[i].bToken; + + if (p.cToken == p.bToken) { + /// @dev start event recorder + vm.recordLogs(); + // Setup: open position + _fund(owner, p.cToken, assets.maxCAmts(p.cToken)); + IERC20(p.cToken).approve(p.addr, assets.maxCAmts(p.cToken)); + IPosition(p.addr).add(assets.maxCAmts(p.cToken), TEST_LTV, 0, TEST_POOL_FEE, TEST_CLIENT); + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + + // Extract amount of base token added to Aave + bytes memory addEvent = entries[entries.length - 1].data; + uint256 suppliedBAmt; + assembly { + suppliedBAmt := mload(add(addEvent, 0x60)) + } + + // Get pre-act balances + contractBalances.preBToken = IERC20(p.bToken).balanceOf(p.addr); + contractBalances.preDToken = IERC20(p.dToken).balanceOf(p.addr); + contractBalances.preVDToken = _getVariableDebtTokenBalance(p.addr, p.dToken); + contractBalances.preCAToken = _getATokenBalance(p.addr, p.cToken); + contractBalances.preBAToken = _getATokenBalance(p.addr, p.bToken); + ownerBalances.preBToken = IERC20(p.bToken).balanceOf(owner); + ownerBalances.preCToken = IERC20(p.cToken).balanceOf(owner); + + // Assertions + assertEq(ownerBalances.preBToken, 0); + assertEq(contractBalances.preDToken, 0); + assertEq(contractBalances.preBToken, 0); + assertNotEq(contractBalances.preVDToken, 0); + assertNotEq(contractBalances.preCAToken, 0); + assertNotEq(contractBalances.preBAToken, 0); + assertEq(ownerBalances.preBToken, ownerBalances.preCToken); + + // Mock Uniswap to ensure position losses + uint256 dAmtOut = contractBalances.preVDToken * REPAY_PERCENT / 100; + _fund(SWAP_ROUTER, p.dToken, dAmtOut); + vm.etch(SWAP_ROUTER, address(new MockUniswapLosses()).code); + + // Withdraw B_TOKEN and make repayment manually to get exact withdrawCAmt + snapshots.id2 = vm.snapshot(); + IPosition(p.addr).withdraw(p.bToken, suppliedBAmt, owner); + _fund(owner, p.dToken, dAmtOut); + IERC20(p.dToken).approve(p.addr, dAmtOut); + IPosition(p.addr).repay(dAmtOut); + uint256 maxCTokenWithdrawal = _getMaxWithdrawAmt(p.addr, p.cToken, assets.decimals(p.cToken)); + vm.revertTo(snapshots.id2); + + // Bound fuzzed variables + withdrawCAmt = bound(withdrawCAmt, 1, maxCTokenWithdrawal); + + // Act + IPosition(p.addr).reduce(TEST_POOL_FEE, false, 0, withdrawCAmt, suppliedBAmt); + entries = vm.getRecordedLogs(); + + // Get post-act balances + contractBalances.postBToken = IERC20(p.bToken).balanceOf(p.addr); + contractBalances.postDToken = IERC20(p.dToken).balanceOf(p.addr); + contractBalances.postVDToken = _getVariableDebtTokenBalance(p.addr, p.dToken); + contractBalances.postCAToken = _getATokenBalance(p.addr, p.cToken); + contractBalances.postBAToken = _getATokenBalance(p.addr, p.bToken); + ownerBalances.postBToken = IERC20(p.bToken).balanceOf(owner); + ownerBalances.postCToken = IERC20(p.cToken).balanceOf(owner); + + bytes memory reduceEvent = entries[entries.length - 1].data; + uint256 gains; + + assembly { + gains := mload(add(reduceEvent, 0x20)) + } + + // Assertions + assertApproxEqAbs( + contractBalances.postBAToken, contractBalances.preBAToken - suppliedBAmt - withdrawCAmt, 1 + ); + assertApproxEqAbs( + contractBalances.postCAToken, contractBalances.preBAToken - suppliedBAmt - withdrawCAmt, 1 + ); + assertEq(contractBalances.postDToken, 0); + assertApproxEqAbs(contractBalances.postVDToken, contractBalances.preVDToken - dAmtOut, 1); + assertGt(contractBalances.postVDToken, 0); + assertEq(gains, 0); + assertEq(ownerBalances.postBToken, ownerBalances.preBToken + withdrawCAmt); + assertEq(ownerBalances.postCToken, ownerBalances.postBToken); + + // Revert to snapshot + vm.revertTo(snapshots.id1); + } + } + } +} diff --git a/test/integration/Position.reduce.partial.sol b/test/integration/Position.reduce.partial.sol new file mode 100644 index 0000000..4744521 --- /dev/null +++ b/test/integration/Position.reduce.partial.sol @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.21; + +// External Imports +import { Test } from "forge-std/Test.sol"; +import { VmSafe } from "forge-std/Vm.sol"; + +// Local Imports +import { PositionFactory } from "src/PositionFactory.sol"; +import { + Assets, + AAVE_ORACLE, + CONTRACT_DEPLOYER, + DAI, + FEE_COLLECTOR, + PROTOCOL_FEE_RATE, + SWAP_ROUTER, + TEST_CLIENT, + TEST_LTV, + TEST_POOL_FEE, + USDC +} from "test/common/Constants.t.sol"; +import { TokenUtils } from "test/common/utils/TokenUtils.t.sol"; +import { DebtUtils } from "test/common/utils/DebtUtils.t.sol"; +import { MockUniswapDirectSwap } from "test/mocks/MockUniswap.t.sol"; +import { IAaveOracle } from "src/interfaces/aave/IAaveOracle.sol"; +import { IPosition } from "src/interfaces/IPosition.sol"; +import { IERC20 } from "src/interfaces/token/IERC20.sol"; + +contract PositionReduceGainsTest is Test, TokenUtils, DebtUtils { + /* solhint-disable func-name-mixedcase */ + + struct TestPosition { + address addr; + address cToken; + address dToken; + address bToken; + } + + struct ContractBalances { + uint256 preBToken; + uint256 postBToken; + uint256 preVDToken; + uint256 postVDToken; + uint256 preCAToken; + uint256 postCAToken; + uint256 preBAToken; + uint256 postBAToken; + uint256 preDToken; + uint256 postDToken; + } + + struct OwnerBalances { + uint256 preBToken; + uint256 postBToken; + uint256 preCToken; + uint256 postCToken; + } + + struct RepayData { + uint256 debtInB; + uint256 repayAmtUSD; + uint256 repayAmtInDToken; + uint256 maxWithdrawBAmt; + uint256 maxWithdrawCAmt; + uint256 bATokenAfterRepay; + } + + // Test contracts + PositionFactory public positionFactory; + Assets public assets; + TestPosition[] public positions; + + // Test Storage + address public positionAddr; + address public owner = address(this); + + function setUp() public { + // Deploy assets + assets = new Assets(); + address[4] memory supportedAssets = assets.getSupported(); + + // Deploy FeeCollector + vm.prank(CONTRACT_DEPLOYER); + deployCodeTo("FeeCollector.sol", abi.encode(CONTRACT_DEPLOYER, PROTOCOL_FEE_RATE), FEE_COLLECTOR); + + // Deploy PositionFactory + vm.prank(CONTRACT_DEPLOYER); + positionFactory = new PositionFactory(CONTRACT_DEPLOYER); + + // Deploy and store all possible positions + for (uint256 i; i < supportedAssets.length; i++) { + address cToken = supportedAssets[i]; + for (uint256 j; j < supportedAssets.length; j++) { + if (j != i) { + address dToken = supportedAssets[j]; + for (uint256 k; k < supportedAssets.length; k++) { + address bToken = supportedAssets[k]; + // Exclude positions with no pool + bool poolExists = !((dToken == USDC && bToken == DAI) || (dToken == DAI && bToken == USDC)); + if (k != j && poolExists) { + positionAddr = positionFactory.createPosition(cToken, dToken, bToken); + TestPosition memory newPosition = + TestPosition({ addr: positionAddr, cToken: cToken, dToken: dToken, bToken: bToken }); + positions.push(newPosition); + } + } + } + } + } + + // Mock AaveOracle + for (uint256 i; i < supportedAssets.length; i++) { + vm.mockCall( + AAVE_ORACLE, + abi.encodeWithSelector(IAaveOracle(AAVE_ORACLE).getAssetPrice.selector, supportedAssets[i]), + abi.encode(assets.prices(supportedAssets[i])) + ); + } + } + + /// @dev: Simulates reduction where not all B_TOKEN is withdrawn and swapped for D_TOKEN, + // where D_TOKEN amount is less than total debt. + // - Position contract's (bToken) AToken balance should decrease by _withdrawBAmt. + // - Position contract's (cToken) AToken balance should decrease by _withdrawCAmt. + /// @notice if B_TOKEN withdraw value <= debt value, dAmtOut == debt repayment, so dToken balance == 0. + // - Position contract's dToken balance should be 0. + // - Position contract's bToken balance should remain 0. + // - Position contract's debt on Aave should decrease by amount repaid. + // - Owner's cToken balance should increase by _withdrawCAmt. + // - Owner's bToken balance should stay the same (no gains). + // - Gains should be 0 because if there are any, they are unrealized. + // - The above should be true for all supported tokens. + function testFuzz_ReducePartialExactInputDiffCAndB(uint256 _withdrawBAmt, uint256 _withdrawCAmt) public { + // Setup + ContractBalances memory contractBalances; + OwnerBalances memory ownerBalances; + TestPosition memory p; + RepayData memory repayData; + + // Take snapshot + uint256 id = vm.snapshot(); + for (uint256 i; i < positions.length; i++) { + // Test variables + p.addr = positions[i].addr; + p.cToken = positions[i].cToken; + p.dToken = positions[i].dToken; + p.bToken = positions[i].bToken; + + if (p.cToken != p.bToken) { + // Add to position + _fund(owner, p.cToken, assets.maxCAmts(p.cToken)); + IERC20(p.cToken).approve(p.addr, assets.maxCAmts(p.cToken)); + IPosition(p.addr).add(assets.maxCAmts(p.cToken), TEST_LTV, 0, TEST_POOL_FEE, TEST_CLIENT); + + // Get pre-act balances + contractBalances.preBToken = IERC20(p.bToken).balanceOf(p.addr); + contractBalances.preDToken = IERC20(p.dToken).balanceOf(p.addr); + contractBalances.preVDToken = _getVariableDebtTokenBalance(p.addr, p.dToken); + contractBalances.preCAToken = _getATokenBalance(p.addr, p.cToken); + contractBalances.preBAToken = _getATokenBalance(p.addr, p.bToken); + ownerBalances.preBToken = IERC20(p.bToken).balanceOf(owner); + ownerBalances.preCToken = IERC20(p.cToken).balanceOf(owner); + + // Assertions + assertEq(ownerBalances.preBToken, 0); + assertEq(contractBalances.preDToken, 0); + assertEq(contractBalances.preBToken, 0); + assertNotEq(contractBalances.preVDToken, 0); + assertNotEq(contractBalances.preCAToken, 0); + assertNotEq(contractBalances.preBAToken, 0); + + // Calculate debt in terms of bToken and bound fuzzed _withdrawBAmt variable + repayData.debtInB = _getDebtInB(p.addr, p.bToken, assets.decimals(p.bToken)); + repayData.maxWithdrawBAmt = + repayData.debtInB <= contractBalances.preBAToken ? repayData.debtInB : contractBalances.preBAToken; + _withdrawBAmt = bound(_withdrawBAmt, assets.minCAmts(p.bToken), repayData.maxWithdrawBAmt); + + // Calculate repay amount and bound fuzzed _withdrawCAmt variable + uint256 repayID = vm.snapshot(); + repayData.repayAmtUSD = (_withdrawBAmt * assets.prices(p.bToken)) / (10 ** assets.decimals(p.bToken)); + repayData.repayAmtInDToken = + ((repayData.repayAmtUSD * (10 ** assets.decimals(p.dToken))) / assets.prices(p.dToken)); + IPosition(p.addr).withdraw(p.bToken, _withdrawBAmt, owner); + _fund(owner, p.dToken, repayData.repayAmtInDToken); + IERC20(p.dToken).approve(p.addr, repayData.repayAmtInDToken); + IPosition(p.addr).repay(repayData.repayAmtInDToken); + repayData.bATokenAfterRepay = contractBalances.preBAToken - _withdrawBAmt; + repayData.maxWithdrawCAmt = _getMaxWithdrawCAmtAfterPartialRepay( + p.addr, + p.cToken, + p.bToken, + assets.decimals(p.cToken), + assets.decimals(p.bToken), + repayData.bATokenAfterRepay + ); + vm.revertTo(repayID); + _withdrawCAmt = bound(_withdrawCAmt, assets.minCAmts(p.cToken), repayData.maxWithdrawCAmt); + + // Mock Uniswap + _fund(SWAP_ROUTER, p.dToken, repayData.repayAmtInDToken); + bytes memory code = address(new MockUniswapDirectSwap()).code; + vm.etch(SWAP_ROUTER, code); + + // Act + /// @dev start event recorder + vm.recordLogs(); + IPosition(p.addr).reduce(TEST_POOL_FEE, false, 0, _withdrawCAmt, _withdrawBAmt); + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + + // Get post-act balances + contractBalances.postBToken = IERC20(p.bToken).balanceOf(p.addr); + contractBalances.postVDToken = _getVariableDebtTokenBalance(p.addr, p.dToken); + contractBalances.postCAToken = _getATokenBalance(p.addr, p.cToken); + contractBalances.postBAToken = _getATokenBalance(p.addr, p.bToken); + contractBalances.postDToken = IERC20(p.dToken).balanceOf(p.addr); + ownerBalances.postBToken = IERC20(p.bToken).balanceOf(owner); + ownerBalances.postCToken = IERC20(p.cToken).balanceOf(owner); + + bytes memory reduceEvent = entries[entries.length - 1].data; + uint256 gains; + + assembly { + gains := mload(add(reduceEvent, 0x20)) + } + + // Assertions: + assertEq(contractBalances.postDToken, 0); + assertEq(contractBalances.postBToken, 0); + assertApproxEqAbs( + contractBalances.postVDToken, contractBalances.preVDToken - repayData.repayAmtInDToken, 1 + ); + assertApproxEqAbs(contractBalances.postCAToken, contractBalances.preCAToken - _withdrawCAmt, 1); + assertApproxEqAbs(contractBalances.postBAToken, contractBalances.preBAToken - _withdrawBAmt, 1); + assertEq(gains, 0); + assertEq(ownerBalances.postBToken, ownerBalances.preBToken); + assertEq(ownerBalances.postCToken, ownerBalances.preCToken + _withdrawCAmt); + + // Revert to snapshot + vm.revertTo(id); + } + } + } + + /// @dev: Simulates reduction where not all B_TOKEN is withdrawn and swapped for D_TOKEN, + // where D_TOKEN amount is less than total debt. + // - Position contract's (cToken) AToken balance should decrease by (_withdrawCAmt + _withdrawBAmt). + // - Position contract's (bToken) AToken balance should should equal its (cToken) AToken balance. + /// @notice if B_TOKEN withdraw value <= debt value, dAmtOut == debt repayment, so dToken balance == 0. + // - Position contract's dToken balance should be 0. + // - Position contract's bToken balance should remain 0. + // - Position contract's debt on Aave should decrease by amount repaid. + // - Owner's cToken balance should increase by _withdrawCAmt. + // - Owner's bToken balance should equal owner's cToken balance. + // - Gains should be 0 because if there are any, they are unrealized. + // - The above should be true for all supported tokens. + function testFuzz_ReducePartialExactInputSameCAndB(uint256 _withdrawBAmt, uint256 _withdrawCAmt) public { + // Setup + ContractBalances memory contractBalances; + OwnerBalances memory ownerBalances; + TestPosition memory p; + RepayData memory repayData; + + // Take snapshot + uint256 id = vm.snapshot(); + for (uint256 i; i < positions.length; i++) { + // Test variables + p.addr = positions[i].addr; + p.cToken = positions[i].cToken; + p.dToken = positions[i].dToken; + p.bToken = positions[i].bToken; + + if (p.cToken == p.bToken) { + // Add to position + _fund(owner, p.cToken, assets.maxCAmts(p.cToken)); + IERC20(p.cToken).approve(p.addr, assets.maxCAmts(p.cToken)); + IPosition(p.addr).add(assets.maxCAmts(p.cToken), TEST_LTV, 0, TEST_POOL_FEE, TEST_CLIENT); + + // Get pre-act balances + contractBalances.preBToken = IERC20(p.bToken).balanceOf(p.addr); + contractBalances.preDToken = IERC20(p.dToken).balanceOf(p.addr); + contractBalances.preVDToken = _getVariableDebtTokenBalance(p.addr, p.dToken); + contractBalances.preCAToken = _getATokenBalance(p.addr, p.cToken); + contractBalances.preBAToken = _getATokenBalance(p.addr, p.bToken); + ownerBalances.preBToken = IERC20(p.bToken).balanceOf(owner); + ownerBalances.preCToken = IERC20(p.cToken).balanceOf(owner); + + // Assertions + assertEq(ownerBalances.preBToken, 0); + assertEq(contractBalances.preDToken, 0); + assertEq(contractBalances.preBToken, 0); + assertNotEq(contractBalances.preVDToken, 0); + assertNotEq(contractBalances.preCAToken, 0); + assertNotEq(contractBalances.preBAToken, 0); + + // Calculate debt in terms of bToken and bound fuzzed _withdrawBAmt variable + repayData.debtInB = _getDebtInB(p.addr, p.bToken, assets.decimals(p.bToken)); + repayData.maxWithdrawBAmt = + repayData.debtInB <= contractBalances.preBAToken ? repayData.debtInB : contractBalances.preBAToken; + _withdrawBAmt = bound(_withdrawBAmt, assets.minCAmts(p.bToken), repayData.maxWithdrawBAmt); + + // Calculate repay amount and bound fuzzed _withdrawCAmt variable + uint256 repayID = vm.snapshot(); + repayData.repayAmtUSD = (_withdrawBAmt * assets.prices(p.bToken)) / (10 ** assets.decimals(p.bToken)); + repayData.repayAmtInDToken = + ((repayData.repayAmtUSD * (10 ** assets.decimals(p.dToken))) / assets.prices(p.dToken)); + IPosition(p.addr).withdraw(p.bToken, _withdrawBAmt, owner); + _fund(owner, p.dToken, repayData.repayAmtInDToken); + IERC20(p.dToken).approve(p.addr, repayData.repayAmtInDToken); + IPosition(p.addr).repay(repayData.repayAmtInDToken); + repayData.bATokenAfterRepay = contractBalances.preBAToken - _withdrawBAmt; + repayData.maxWithdrawCAmt = contractBalances.preBAToken - repayData.bATokenAfterRepay; + vm.revertTo(repayID); + _withdrawCAmt = bound(_withdrawCAmt, assets.minCAmts(p.cToken), repayData.maxWithdrawCAmt); + + // Mock Uniswap + _fund(SWAP_ROUTER, p.dToken, repayData.repayAmtInDToken); + bytes memory code = address(new MockUniswapDirectSwap()).code; + vm.etch(SWAP_ROUTER, code); + + // Act + /// @dev start event recorder + vm.recordLogs(); + IPosition(p.addr).reduce(TEST_POOL_FEE, false, 0, _withdrawCAmt, _withdrawBAmt); + VmSafe.Log[] memory entries = vm.getRecordedLogs(); + + // Get post-act balances + contractBalances.postBToken = IERC20(p.bToken).balanceOf(p.addr); + contractBalances.postVDToken = _getVariableDebtTokenBalance(p.addr, p.dToken); + contractBalances.postCAToken = _getATokenBalance(p.addr, p.cToken); + contractBalances.postBAToken = _getATokenBalance(p.addr, p.bToken); + contractBalances.postDToken = IERC20(p.dToken).balanceOf(p.addr); + ownerBalances.postBToken = IERC20(p.bToken).balanceOf(owner); + ownerBalances.postCToken = IERC20(p.cToken).balanceOf(owner); + + bytes memory reduceEvent = entries[entries.length - 1].data; + uint256 gains; + + assembly { + gains := mload(add(reduceEvent, 0x20)) + } + + // Assertions: + assertEq(contractBalances.postDToken, 0); + assertEq(contractBalances.postBToken, 0); + assertApproxEqAbs( + contractBalances.postVDToken, contractBalances.preVDToken - repayData.repayAmtInDToken, 1 + ); + assertApproxEqAbs( + contractBalances.postCAToken, contractBalances.preCAToken - _withdrawCAmt - _withdrawBAmt, 1 + ); + assertEq(contractBalances.postBAToken, contractBalances.postCAToken); + assertEq(gains, 0); + assertEq(ownerBalances.postBToken, ownerBalances.postCToken); + assertEq(ownerBalances.postCToken, ownerBalances.preCToken + _withdrawCAmt); + + // Revert to snapshot + vm.revertTo(id); + } + } + } +}