diff --git a/test/BaseTest.t.sol b/test/BaseTest.t.sol new file mode 100644 index 0000000..b42eb44 --- /dev/null +++ b/test/BaseTest.t.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; +import "utils/src/CommonUtils.sol"; +import { IERC20 } from "forge-std/interfaces/IERC20.sol"; + +contract BaseTest is Test, CommonUtils { + // Useful addresses + address public alice = makeAddr("alice"); + address public bob = makeAddr("bob"); + address public integrator = makeAddr("integrator"); + address public keeper = makeAddr("keeper"); + address public developer = makeAddr("developer"); + + function setUp() public virtual { + vm.label(alice, "alice"); + vm.label(bob, "bob"); + vm.label(integrator, "integrator"); + vm.label(keeper, "keeper"); + vm.label(developer, "developer"); + } +} diff --git a/test/Constants.t.sol b/test/Constants.t.sol new file mode 100644 index 0000000..bb13734 --- /dev/null +++ b/test/Constants.t.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +uint256 constant CHAIN_SOURCE = 1; +address constant ONEINCH_ROUTER = 0x111111125421cA6dc452d289314280a0f8842A65; +address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; diff --git a/test/ERC4626StrategyTest.t.sol b/test/ERC4626StrategyTest.t.sol new file mode 100644 index 0000000..aba0d7d --- /dev/null +++ b/test/ERC4626StrategyTest.t.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.26; + +import "./BaseTest.t.sol"; +import "./Constants.t.sol"; +import "../contracts/utils/Errors.sol"; +import { IAccessControl } from "oz/access/AccessControl.sol"; +import { ERC4626Strategy, BaseStrategy, ERC4626 } from "../contracts/ERC4626Strategy.sol"; + +contract ERC4626StrategyTest is BaseTest { + ERC4626Strategy public strategy; + address public asset; + address public strategyAsset; + + function setUp() public virtual override { + super.setUp(); + + vm.createSelectFork("mainnet", 20363172); + + asset = _chainToContract(CHAIN_SOURCE, ContractType.AgUSD); + strategyAsset = _chainToContract(CHAIN_SOURCE, ContractType.StUSD); + + strategy = new ERC4626Strategy( + BaseStrategy.ConstructorArgs( + 10000, // 10% + 20000, // 20% + integrator, + developer, + keeper, + developer, + integrator, + ONEINCH_ROUTER, + ONEINCH_ROUTER, + 1 weeks, + "stUSD Strategy", + "stUSDStrat", + asset, + strategyAsset + ) + ); + } +} diff --git a/test/mock/MockRouter.sol b/test/mock/MockRouter.sol new file mode 100644 index 0000000..8724392 --- /dev/null +++ b/test/mock/MockRouter.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import { SafeERC20, IERC20 } from "oz/token/ERC20/utils/SafeERC20.sol"; + +contract MockRouter { + using SafeERC20 for IERC20; + + function swap(uint256 amountIn, address tokenIn, uint256 amountOut, address tokenOut) external { + IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); + IERC20(tokenOut).safeTransfer(msg.sender, amountOut); + } +} diff --git a/test/unit/.gitkeep b/test/unit/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/test/unit/Accumulate.t.sol b/test/unit/Accumulate.t.sol new file mode 100644 index 0000000..8ff8565 --- /dev/null +++ b/test/unit/Accumulate.t.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "../ERC4626StrategyTest.t.sol"; + +contract AccumulateTest is ERC4626StrategyTest { + function test_accumulate_Normal() public { + deal(asset, alice, 100e18); + + vm.startPrank(alice); + IERC20(asset).approve(address(strategy), 100e18); + strategy.deposit(100e18, alice); + vm.stopPrank(); + uint256 balance = ERC4626(strategyAsset).balanceOf(address(strategy)); + uint256 lastTotalAssets = strategy.lastTotalAssets(); + vm.warp(block.timestamp + 1 weeks); + + uint256 totalAssets = strategy.totalAssets(); + strategy.accumulate(); + + uint256 feeShares = strategy.convertToShares( + ((totalAssets - lastTotalAssets) * strategy.performanceFee()) / strategy.BPS() + ); + uint256 developerFeeShares = (feeShares * strategy.developerFee()) / strategy.BPS(); + + assertEq(strategy.lastTotalAssets(), ERC4626(strategyAsset).convertToAssets(balance)); + assertEq(strategy.balanceOf(strategy.integratorFeeRecipient()), feeShares - developerFeeShares); + assertEq(strategy.balanceOf(strategy.developerFeeRecipient()), developerFeeShares); + } + + function test_accumulate_NegativeProfit() public { + deal(asset, alice, 100e18); + + vm.startPrank(alice); + IERC20(asset).approve(address(strategy), 100e18); + strategy.deposit(100e18, alice); + vm.stopPrank(); + uint256 balance = ERC4626(strategyAsset).balanceOf(address(strategy)); + uint256 lastTotalAssets = strategy.lastTotalAssets(); + vm.warp(block.timestamp + 1 weeks); + + vm.mockCall(strategyAsset, abi.encodeWithSelector(IERC20.balanceOf.selector), abi.encode(9e18)); + strategy.accumulate(); + + assertEq(strategy.balanceOf(strategy.integratorFeeRecipient()), 0); + assertEq(strategy.balanceOf(strategy.developerFeeRecipient()), 0); + } +} diff --git a/test/unit/Constructor.t.sol b/test/unit/Constructor.t.sol new file mode 100644 index 0000000..83f5435 --- /dev/null +++ b/test/unit/Constructor.t.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "../ERC4626StrategyTest.t.sol"; + +contract ConstructorTest is ERC4626StrategyTest { + function test_constructor_Normal() public { + assertEq(strategy.vestingPeriod(), 1 weeks); + assertEq(strategy.performanceFee(), 10_000); + assertEq(strategy.developerFee(), 20_000); + assertEq(strategy.developerFeeRecipient(), developer); + assertEq(strategy.integratorFeeRecipient(), integrator); + assertEq(strategy.swapRouter(), ONEINCH_ROUTER); + assertEq(strategy.tokenTransferAddress(), ONEINCH_ROUTER); + assertEq(strategy.STRATEGY_ASSET(), strategyAsset); + assertEq(strategy.decimals(), 18); + assertEq(strategy.name(), "stUSD Strategy"); + assertEq(strategy.symbol(), "stUSDStrat"); + assertEq(strategy.lockedProfit(), 0); + } + + function test_constructor_CorrectRoles() public { + assertTrue(strategy.hasRole(strategy.DEVELOPER_ROLE(), developer)); + assertTrue(strategy.hasRole(strategy.INTEGRATOR_ROLE(), integrator)); + assertTrue(strategy.hasRole(strategy.KEEPER_ROLE(), keeper)); + + assertEq(strategy.getRoleAdmin(strategy.DEVELOPER_ROLE()), strategy.DEVELOPER_ROLE()); + assertEq(strategy.getRoleAdmin(strategy.INTEGRATOR_ROLE()), strategy.INTEGRATOR_ROLE()); + assertEq(strategy.getRoleAdmin(strategy.KEEPER_ROLE()), strategy.DEVELOPER_ROLE()); + } + + function test_constructor_DifferentDecimals() public { + vm.mockCall(asset, abi.encodeWithSelector(IERC20.decimals.selector), abi.encode(2)); + strategy = new ERC4626Strategy( + BaseStrategy.ConstructorArgs( + 10000, // 10% + 20000, // 20% + integrator, + developer, + keeper, + developer, + integrator, + ONEINCH_ROUTER, + ONEINCH_ROUTER, + 1 weeks, + "stUSD Strategy", + "stUSDStrat", + asset, + strategyAsset + ) + ); + + assertEq(strategy.decimals(), 18); + } + + function test_constructor_MaxPerformanceFee() public { + vm.expectRevert(InvalidFee.selector); + strategy = new ERC4626Strategy( + BaseStrategy.ConstructorArgs( + 100_001, // 100.001% + 20_000, // 20% + integrator, + developer, + keeper, + developer, + integrator, + ONEINCH_ROUTER, + ONEINCH_ROUTER, + 1 weeks, + "stUSD Strategy", + "stUSDStrat", + asset, + strategyAsset + ) + ); + } + + function test_constructor_MaxDeveloperFee() public { + vm.expectRevert(InvalidFee.selector); + strategy = new ERC4626Strategy( + BaseStrategy.ConstructorArgs( + 100_000, // 100% + 50_001, // 20% + integrator, + developer, + keeper, + developer, + integrator, + ONEINCH_ROUTER, + ONEINCH_ROUTER, + 1 weeks, + "stUSD Strategy", + "stUSDStrat", + asset, + strategyAsset + ) + ); + } + + function test_constructor_ZeroAdress() public { + vm.expectRevert(ZeroAddress.selector); + strategy = new ERC4626Strategy( + BaseStrategy.ConstructorArgs( + 10000, // 10% + 20_000, // 20% + address(0), + developer, + keeper, + developer, + integrator, + ONEINCH_ROUTER, + ONEINCH_ROUTER, + 1 weeks, + "stUSD Strategy", + "stUSDStrat", + asset, + strategyAsset + ) + ); + + vm.expectRevert(ZeroAddress.selector); + strategy = new ERC4626Strategy( + BaseStrategy.ConstructorArgs( + 10000, // 10% + 20_000, // 20% + integrator, + address(0), + keeper, + developer, + integrator, + ONEINCH_ROUTER, + ONEINCH_ROUTER, + 1 weeks, + "stUSD Strategy", + "stUSDStrat", + asset, + strategyAsset + ) + ); + + vm.expectRevert(ZeroAddress.selector); + strategy = new ERC4626Strategy( + BaseStrategy.ConstructorArgs( + 10000, // 10% + 20_000, // 20% + integrator, + developer, + address(0), + developer, + integrator, + ONEINCH_ROUTER, + ONEINCH_ROUTER, + 1 weeks, + "stUSD Strategy", + "stUSDStrat", + asset, + strategyAsset + ) + ); + + vm.expectRevert(ZeroAddress.selector); + strategy = new ERC4626Strategy( + BaseStrategy.ConstructorArgs( + 10000, // 10% + 20_000, // 20% + integrator, + developer, + keeper, + address(0), + integrator, + ONEINCH_ROUTER, + ONEINCH_ROUTER, + 1 weeks, + "stUSD Strategy", + "stUSDStrat", + asset, + strategyAsset + ) + ); + + vm.expectRevert(ZeroAddress.selector); + strategy = new ERC4626Strategy( + BaseStrategy.ConstructorArgs( + 10000, // 10% + 20_000, // 20% + integrator, + developer, + keeper, + developer, + address(0), + ONEINCH_ROUTER, + ONEINCH_ROUTER, + 1 weeks, + "stUSD Strategy", + "stUSDStrat", + asset, + strategyAsset + ) + ); + + vm.expectRevert(ZeroAddress.selector); + strategy = new ERC4626Strategy( + BaseStrategy.ConstructorArgs( + 10000, // 10% + 20_000, // 20% + integrator, + developer, + keeper, + developer, + integrator, + address(0), + ONEINCH_ROUTER, + 1 weeks, + "stUSD Strategy", + "stUSDStrat", + asset, + strategyAsset + ) + ); + + vm.expectRevert(ZeroAddress.selector); + strategy = new ERC4626Strategy( + BaseStrategy.ConstructorArgs( + 10000, // 10% + 20_000, // 20% + integrator, + developer, + keeper, + developer, + integrator, + ONEINCH_ROUTER, + address(0), + 1 weeks, + "stUSD Strategy", + "stUSDStrat", + asset, + strategyAsset + ) + ); + } +} diff --git a/test/unit/Deposit.t.sol b/test/unit/Deposit.t.sol new file mode 100644 index 0000000..942d501 --- /dev/null +++ b/test/unit/Deposit.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "../ERC4626StrategyTest.t.sol"; + +contract DepositTest is ERC4626StrategyTest { + function test_Deposit_Normal() public { + deal(asset, alice, 100e18); + + vm.startPrank(alice); + IERC20(asset).approve(address(strategy), 100e18); + + uint256 previewedDeposit = strategy.previewDeposit(100e18); + strategy.deposit(100e18, alice); + vm.stopPrank(); + + assertEq(ERC4626(strategyAsset).balanceOf(address(strategy)), ERC4626(strategyAsset).convertToShares(100e18)); + assertEq(IERC20(asset).balanceOf(alice), 0); + assertEq(IERC20(asset).balanceOf(address(strategy)), 0); + assertEq(strategy.balanceOf(alice), previewedDeposit); + assertEq(strategy.totalSupply(), previewedDeposit); + } + + function test_Deposit_MultipleProfit() public { + deal(asset, alice, 200e18); + + vm.startPrank(alice); + IERC20(asset).approve(address(strategy), 200e18); + strategy.deposit(100e18, bob); + vm.stopPrank(); + + vm.warp(block.timestamp + 1 weeks); + + uint256 totalAssets = strategy.totalAssets(); + uint256 lastTotalAssets = strategy.lastTotalAssets(); + uint256 previewedDeposit = strategy.previewDeposit(100e18); + uint256 previousBalance = ERC4626(strategyAsset).balanceOf(address(strategy)); + + vm.prank(alice); + strategy.deposit(100e18, alice); + + uint256 feeShares = strategy.convertToShares( + ((totalAssets - lastTotalAssets) * strategy.performanceFee()) / strategy.BPS() + ); + uint256 developerFeeShares = (feeShares * strategy.developerFee()) / strategy.BPS(); + + assertLe(strategy.lastTotalAssets() - 1, strategy.totalAssets()); + assertGe(strategy.lastTotalAssets(), strategy.totalAssets()); + assertEq(strategy.balanceOf(alice), previewedDeposit); + assertEq(strategy.balanceOf(strategy.integratorFeeRecipient()), feeShares - developerFeeShares); + assertEq(strategy.balanceOf(strategy.developerFeeRecipient()), developerFeeShares); + assertEq( + ERC4626(strategyAsset).balanceOf(address(strategy)), + previousBalance + ERC4626(strategyAsset).convertToShares(100e18) + ); + } + + function test_Deposit_MultipleLoss() public { + deal(asset, alice, 200e18); + + vm.startPrank(alice); + IERC20(asset).approve(address(strategy), 200e18); + strategy.deposit(100e18, bob); + vm.stopPrank(); + + vm.warp(block.timestamp + 1 weeks); + + vm.mockCall(strategyAsset, abi.encodeWithSelector(IERC20.balanceOf.selector), abi.encode(9e18)); + uint256 previewedDeposit = strategy.previewDeposit(100e18); + uint256 previousBalance = ERC4626(strategyAsset).balanceOf(address(strategy)); + + vm.prank(alice); + vm.mockCall(strategyAsset, abi.encodeWithSelector(IERC20.balanceOf.selector), abi.encode(9e18)); + strategy.deposit(100e18, alice); + + assertEq(strategy.balanceOf(alice), previewedDeposit); + assertEq(strategy.balanceOf(strategy.integratorFeeRecipient()), 0); + assertEq(strategy.balanceOf(strategy.developerFeeRecipient()), 0); + } +} diff --git a/test/unit/MaxDeposit.t.sol b/test/unit/MaxDeposit.t.sol new file mode 100644 index 0000000..316abee --- /dev/null +++ b/test/unit/MaxDeposit.t.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "../ERC4626StrategyTest.t.sol"; + +contract MaxDepositTest is ERC4626StrategyTest { + function test_MaxDeposit_Normal() public { + assertEq(strategy.maxDeposit(alice), ERC4626(strategyAsset).maxDeposit(address(strategy))); + } +} diff --git a/test/unit/MaxMint.t.sol b/test/unit/MaxMint.t.sol new file mode 100644 index 0000000..ae97564 --- /dev/null +++ b/test/unit/MaxMint.t.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "../ERC4626StrategyTest.t.sol"; + +contract MaxMintTest is ERC4626StrategyTest { + function test_MaxMint_Normal() public { + assertEq(strategy.maxMint(alice), ERC4626(strategyAsset).maxMint(address(strategy))); + } + + function test_MaxMint_AfterDeposit() public { + deal(asset, alice, 100e18); + + vm.startPrank(alice); + IERC20(asset).approve(address(strategy), 100e18); + strategy.deposit(100e18, alice); + vm.stopPrank(); + + vm.warp(block.timestamp + 1 weeks); + + assertEq( + strategy.maxMint(alice), + strategy.convertToShares(ERC4626(strategyAsset).maxDeposit(address(strategy))) + ); + } +} diff --git a/test/unit/MaxRedeem.t.sol b/test/unit/MaxRedeem.t.sol new file mode 100644 index 0000000..839e03d --- /dev/null +++ b/test/unit/MaxRedeem.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "../ERC4626StrategyTest.t.sol"; + +contract MaxRedeemTest is ERC4626StrategyTest { + function test_MaxRedeem_Normal() public { + deal(asset, alice, 100e18); + + vm.startPrank(alice); + IERC20(asset).approve(address(strategy), 100e18); + strategy.deposit(100e18, alice); + vm.stopPrank(); + + assertEq(strategy.maxRedeem(alice), strategy.balanceOf(alice) - 1); + } + + function test_MaxRedeem_HigherThanMaxWithdraw() public { + deal(asset, alice, 100e18); + + vm.startPrank(alice); + IERC20(asset).approve(address(strategy), 100e18); + strategy.deposit(100e18, alice); + vm.stopPrank(); + + vm.mockCall( + strategyAsset, + abi.encodeWithSelector(ERC4626.maxWithdraw.selector, address(strategy)), + abi.encode(50e18) + ); + assertEq(strategy.maxRedeem(alice), strategy.convertToShares(50e18)); + } +} diff --git a/test/unit/MaxWithdraw.t.sol b/test/unit/MaxWithdraw.t.sol new file mode 100644 index 0000000..83429b3 --- /dev/null +++ b/test/unit/MaxWithdraw.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "../ERC4626StrategyTest.t.sol"; + +contract MaxWithdrawTest is ERC4626StrategyTest { + function test_MaxWithdraw_Normal() public { + deal(asset, alice, 100e18); + + vm.startPrank(alice); + IERC20(asset).approve(address(strategy), 100e18); + strategy.deposit(100e18, alice); + vm.stopPrank(); + + assertEq(strategy.maxWithdraw(alice), strategy.convertToAssets(strategy.balanceOf(alice))); + } + + function test_MaxWithdraw_HigherThanMaxWithdraw() public { + deal(asset, alice, 100e18); + + vm.startPrank(alice); + IERC20(asset).approve(address(strategy), 100e18); + strategy.deposit(100e18, alice); + vm.stopPrank(); + + vm.mockCall( + strategyAsset, + abi.encodeWithSelector(ERC4626.maxWithdraw.selector, address(strategy)), + abi.encode(50e18) + ); + assertEq(strategy.maxWithdraw(alice), 50e18); + } +} diff --git a/test/unit/Mint.t.sol b/test/unit/Mint.t.sol new file mode 100644 index 0000000..370c02e --- /dev/null +++ b/test/unit/Mint.t.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "../ERC4626StrategyTest.t.sol"; + +contract MintTest is ERC4626StrategyTest { + function test_Mint_Normal() public { + deal(asset, alice, 100e18); + + vm.startPrank(alice); + IERC20(asset).approve(address(strategy), 100e18); + + uint256 shares = strategy.convertToShares(100e18); + uint256 previewedMint = strategy.previewMint(shares); + uint256 assetsMinted = strategy.mint(shares, alice); + vm.stopPrank(); + + assertEq(ERC4626(strategyAsset).balanceOf(address(strategy)), ERC4626(strategyAsset).convertToShares(100e18)); + assertEq(IERC20(asset).balanceOf(alice), 0); + assertEq(IERC20(asset).balanceOf(address(strategy)), 0); + assertEq(strategy.balanceOf(alice), shares); + assertEq(strategy.totalSupply(), shares); + assertEq(assetsMinted, previewedMint); + } + + function test_Mint_MultipleProfit() public { + deal(asset, alice, 200e18); + + vm.startPrank(alice); + IERC20(asset).approve(address(strategy), 200e18); + + uint256 shares = strategy.convertToShares(100e18); + strategy.mint(shares, bob); + vm.stopPrank(); + + vm.warp(block.timestamp + 1 weeks); + + uint256 totalAssets = strategy.totalAssets(); + uint256 lastTotalAssets = strategy.lastTotalAssets(); + shares = strategy.convertToShares(100e18); + uint256 previewedMint = strategy.previewMint(shares); + uint256 previousBalance = ERC4626(strategyAsset).balanceOf(address(strategy)); + + vm.prank(alice); + uint256 assetsMinted = strategy.mint(shares, alice); + + uint256 feeShares = strategy.convertToShares( + ((totalAssets - lastTotalAssets) * strategy.performanceFee()) / strategy.BPS() + ); + uint256 developerFeeShares = (feeShares * strategy.developerFee()) / strategy.BPS(); + + assertEq(assetsMinted, previewedMint); + assertLe(strategy.lastTotalAssets() - 1, strategy.totalAssets()); + assertGe(strategy.lastTotalAssets(), strategy.totalAssets()); + assertEq(strategy.balanceOf(alice), shares); + assertEq(strategy.balanceOf(strategy.integratorFeeRecipient()), feeShares - developerFeeShares); + assertEq(strategy.balanceOf(strategy.developerFeeRecipient()), developerFeeShares); + assertEq( + ERC4626(strategyAsset).balanceOf(address(strategy)), + previousBalance + ERC4626(strategyAsset).convertToShares(previewedMint) + ); + } + + function test_Mint_MultipleLoss() public { + deal(asset, alice, 200e18); + + vm.startPrank(alice); + IERC20(asset).approve(address(strategy), 200e18); + + uint256 shares = strategy.convertToShares(100e18); + strategy.mint(shares, bob); + vm.stopPrank(); + + vm.warp(block.timestamp + 1 weeks); + + vm.mockCall(strategyAsset, abi.encodeWithSelector(IERC20.balanceOf.selector), abi.encode(9e18)); + shares = strategy.convertToShares(100e18); + uint256 previewedMint = strategy.previewMint(shares); + + vm.prank(alice); + vm.mockCall(strategyAsset, abi.encodeWithSelector(IERC20.balanceOf.selector), abi.encode(9e18)); + uint256 assetsMinted = strategy.mint(shares, alice); + + assertEq(assetsMinted, previewedMint); + assertEq(strategy.balanceOf(alice), shares); + assertEq(strategy.balanceOf(strategy.integratorFeeRecipient()), 0); + assertEq(strategy.balanceOf(strategy.developerFeeRecipient()), 0); + } +} diff --git a/test/unit/Redeem.t.sol b/test/unit/Redeem.t.sol new file mode 100644 index 0000000..667c82c --- /dev/null +++ b/test/unit/Redeem.t.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "../ERC4626StrategyTest.t.sol"; + +contract RedeemTest is ERC4626StrategyTest { + function setUp() public override { + super.setUp(); + + deal(asset, alice, 100e18); + + vm.startPrank(alice); + IERC20(asset).approve(address(strategy), 100e18); + strategy.deposit(100e18, alice); + vm.stopPrank(); + + vm.warp(block.timestamp + 1 weeks); + } + + function test_Redeem_Normal() public { + uint256 previousBalance = strategy.balanceOf(alice); + + uint256 totalAssets = strategy.totalAssets(); + uint256 lastTotalAssets = strategy.lastTotalAssets(); + + vm.startPrank(alice); + uint256 previewedRedeem = strategy.previewRedeem(previousBalance); + uint256 redeemed = strategy.redeem(previousBalance, alice, alice); + vm.stopPrank(); + + uint256 feeShares = strategy.convertToShares( + ((totalAssets - lastTotalAssets) * strategy.performanceFee()) / strategy.BPS() + ); + uint256 developerFeeShares = (feeShares * strategy.developerFee()) / strategy.BPS(); + + assertEq(previewedRedeem, redeemed); + assertEq(IERC20(asset).balanceOf(alice), previewedRedeem); + assertEq(strategy.balanceOf(alice), 0); + assertEq(strategy.balanceOf(strategy.integratorFeeRecipient()), feeShares - developerFeeShares); + assertEq(strategy.balanceOf(strategy.developerFeeRecipient()), developerFeeShares); + } +} diff --git a/test/unit/SetDeveloperFee.t.sol b/test/unit/SetDeveloperFee.t.sol new file mode 100644 index 0000000..30ed965 --- /dev/null +++ b/test/unit/SetDeveloperFee.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "../ERC4626StrategyTest.t.sol"; + +contract SetDeveloperFeeTest is ERC4626StrategyTest { + function test_setDeveloperFee_Normal() public { + vm.expectEmit(true, true, true, true); + emit BaseStrategy.DeveloperFeeUpdated(10_000); + vm.prank(developer); + strategy.setDeveloperFee(10_000); + assertEq(strategy.developerFee(), 10_000); + } + + function test_setDeveloperFee_NotDeveloper() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + bob, + strategy.DEVELOPER_ROLE() + ) + ); + vm.prank(bob); + strategy.setDeveloperFee(10_000); + } + + function test_setDeveloperFee_InvalidFee() public { + vm.expectRevert(InvalidFee.selector); + vm.prank(developer); + strategy.setDeveloperFee(50_001); + } +} diff --git a/test/unit/SetDeveloperFeeRecipient.t.sol b/test/unit/SetDeveloperFeeRecipient.t.sol new file mode 100644 index 0000000..9b25f4e --- /dev/null +++ b/test/unit/SetDeveloperFeeRecipient.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "../ERC4626StrategyTest.t.sol"; + +contract SetDeveloperFeeRecipientTest is ERC4626StrategyTest { + function test_setDeveloperFeeRecipient_Normal() public { + vm.expectEmit(true, true, true, true); + emit BaseStrategy.DeveloperFeeRecipientUpdated(alice); + vm.prank(developer); + strategy.setDeveloperFeeRecipient(alice); + assertEq(strategy.developerFeeRecipient(), alice); + } + + function test_setDeveloperFeeRecipient_NotDeveloper() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + bob, + strategy.DEVELOPER_ROLE() + ) + ); + vm.prank(bob); + strategy.setDeveloperFeeRecipient(alice); + } + + function test_setDeveloperFeeRecipient_ZeroAddress() public { + vm.expectRevert(ZeroAddress.selector); + vm.prank(developer); + strategy.setDeveloperFeeRecipient(address(0)); + } +} diff --git a/test/unit/SetIntegratorFeeRecipient.t.sol b/test/unit/SetIntegratorFeeRecipient.t.sol new file mode 100644 index 0000000..5bb9022 --- /dev/null +++ b/test/unit/SetIntegratorFeeRecipient.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "../ERC4626StrategyTest.t.sol"; + +contract SetIntegratorFeeRecipientTest is ERC4626StrategyTest { + function test_setIntegratorFeeRecipient_Normal() public { + vm.expectEmit(true, true, true, true); + emit BaseStrategy.IntegratorFeeRecipientUpdated(alice); + vm.prank(integrator); + strategy.setIntegratorFeeRecipient(alice); + assertEq(strategy.integratorFeeRecipient(), alice); + } + + function test_setIntegratorFeeRecipient_NotIntegrator() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + bob, + strategy.INTEGRATOR_ROLE() + ) + ); + vm.prank(bob); + strategy.setIntegratorFeeRecipient(alice); + } + + function test_setIntegratorFeeRecipient_ZeroAddress() public { + vm.expectRevert(ZeroAddress.selector); + vm.prank(integrator); + strategy.setIntegratorFeeRecipient(address(0)); + } +} diff --git a/test/unit/SetPerformanceFee.t.sol b/test/unit/SetPerformanceFee.t.sol new file mode 100644 index 0000000..6be1500 --- /dev/null +++ b/test/unit/SetPerformanceFee.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "../ERC4626StrategyTest.t.sol"; + +contract SetPerformanceFeeTest is ERC4626StrategyTest { + function test_setPerformanceFee_Normal() public { + vm.expectEmit(true, true, true, true); + emit BaseStrategy.PerformanceFeeUpdated(10_000); + vm.prank(integrator); + strategy.setPerformanceFee(10_000); + assertEq(strategy.performanceFee(), 10_000); + } + + function test_setPerformanceFee_NotIntegrator() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + bob, + strategy.INTEGRATOR_ROLE() + ) + ); + vm.prank(bob); + strategy.setPerformanceFee(10_000); + } + + function test_setPerformanceFee_InvalidFee() public { + vm.expectRevert(InvalidFee.selector); + vm.prank(integrator); + strategy.setPerformanceFee(100_001); + } +} diff --git a/test/unit/SetSwapRouter.t.sol b/test/unit/SetSwapRouter.t.sol new file mode 100644 index 0000000..da98b39 --- /dev/null +++ b/test/unit/SetSwapRouter.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "../ERC4626StrategyTest.t.sol"; + +contract SetSwapRouterTest is ERC4626StrategyTest { + function test_setSwapRouter_Normal() public { + vm.expectEmit(true, true, true, true); + emit BaseStrategy.SwapRouterUpdated(alice); + vm.prank(developer); + strategy.setSwapRouter(alice); + assertEq(strategy.swapRouter(), alice); + } + + function test_setSwapRouter_NotDeveloper() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + bob, + strategy.DEVELOPER_ROLE() + ) + ); + vm.prank(bob); + strategy.setSwapRouter(alice); + } + + function test_setSwapRouter_ZeroAddress() public { + vm.expectRevert(ZeroAddress.selector); + vm.prank(developer); + strategy.setSwapRouter(address(0)); + } +} diff --git a/test/unit/SetTokenTransferAddress.t.sol b/test/unit/SetTokenTransferAddress.t.sol new file mode 100644 index 0000000..6e3cb8a --- /dev/null +++ b/test/unit/SetTokenTransferAddress.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "../ERC4626StrategyTest.t.sol"; + +contract SetTokenTransferAddressTest is ERC4626StrategyTest { + function test_setTokenTransferAddress_Normal() public { + vm.expectEmit(true, true, true, true); + emit BaseStrategy.TokenTransferAddressUpdated(alice); + vm.prank(developer); + strategy.setTokenTransferAddress(alice); + assertEq(strategy.tokenTransferAddress(), alice); + } + + function test_setTokenTransferAddress_NotDeveloper() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + bob, + strategy.DEVELOPER_ROLE() + ) + ); + vm.prank(bob); + strategy.setTokenTransferAddress(alice); + } + + function test_setTokenTransferAddress_ZeroAddress() public { + vm.expectRevert(ZeroAddress.selector); + vm.prank(developer); + strategy.setTokenTransferAddress(address(0)); + } +} diff --git a/test/unit/SetVestingPeriod.t.sol b/test/unit/SetVestingPeriod.t.sol new file mode 100644 index 0000000..ec06e0c --- /dev/null +++ b/test/unit/SetVestingPeriod.t.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "../ERC4626StrategyTest.t.sol"; + +contract SetVestingPeriodTest is ERC4626StrategyTest { + function test_setVestingPeriod_Normal() public { + vm.expectEmit(true, true, true, true); + emit BaseStrategy.VestingPeriodUpdated(2 weeks); + vm.prank(integrator); + strategy.setVestingPeriod(2 weeks); + assertEq(strategy.vestingPeriod(), 2 weeks); + } + + function test_setVestingPeriod_NotIntegrator() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + bob, + strategy.INTEGRATOR_ROLE() + ) + ); + vm.prank(bob); + strategy.setVestingPeriod(2 weeks); + } +} diff --git a/test/unit/Swap.t.sol b/test/unit/Swap.t.sol new file mode 100644 index 0000000..4d434bd --- /dev/null +++ b/test/unit/Swap.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "../ERC4626StrategyTest.t.sol"; +import { MockRouter } from "../mock/MockRouter.sol"; + +contract SwapTest is ERC4626StrategyTest { + MockRouter router; + + function setUp() public override { + super.setUp(); + + router = new MockRouter(); + vm.startPrank(developer); + strategy.setSwapRouter(address(router)); + strategy.setTokenTransferAddress(address(router)); + vm.stopPrank(); + } + + function test_swap_normal() public { + deal(USDC, address(strategy), 100e18); + deal(asset, address(router), 100e18); + + address[] memory tokens = new address[](1); + tokens[0] = USDC; + uint256[] memory amounts = new uint256[](1); + amounts[0] = 100e18; + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector(MockRouter.swap.selector, 100e18, USDC, 100e18, asset); + + vm.prank(keeper); + strategy.swap(tokens, data, amounts); + + uint256 strategyBalance = ERC4626(strategyAsset).balanceOf(address(strategy)); + + assertEq(IERC20(USDC).allowance(address(strategy), address(router)), 0); + assertEq(strategyBalance, ERC4626(strategyAsset).convertToShares(100e18)); + assertEq(IERC20(asset).balanceOf(address(strategy)), 0); + + assertEq(strategy.vestingProfit(), 100e18); + assertEq(strategy.lastUpdate(), block.timestamp); + assertEq(strategy.lockedProfit(), 100e18); + assertEq(strategy.totalAssets(), 0); + + // Check for linear vesting + vm.warp(block.timestamp + (strategy.vestingPeriod() / 2)); + assertEq(strategy.lockedProfit(), 50e18); + assertEq(strategy.totalAssets(), ERC4626(strategyAsset).convertToAssets(strategyBalance) - 50e18); + + vm.warp(block.timestamp + strategy.vestingPeriod()); + assertEq(strategy.lockedProfit(), 0); + assertEq(strategy.totalAssets(), ERC4626(strategyAsset).convertToAssets(strategyBalance)); + } + + function test_swap_OutgoingAssets() public { + deal(asset, address(strategy), 100e18); + deal(asset, address(router), 100e18); + + address[] memory tokens = new address[](1); + tokens[0] = asset; + uint256[] memory amounts = new uint256[](1); + amounts[0] = 100e18; + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector(MockRouter.swap.selector, 100e18, asset, 10e18, asset); + + vm.expectRevert(OutgoingAssets.selector); + vm.prank(keeper); + strategy.swap(tokens, data, amounts); + } + + function test_swap_OutgoingStrategyAssets() public { + deal(strategyAsset, address(strategy), 100e18); + + address[] memory tokens = new address[](1); + tokens[0] = strategyAsset; + uint256[] memory amounts = new uint256[](1); + amounts[0] = 100e18; + bytes[] memory data = new bytes[](1); + data[0] = abi.encodeWithSelector(MockRouter.swap.selector, 100e18, strategyAsset, 10e18, strategyAsset); + + vm.expectRevert(OutgoingAssets.selector); + vm.prank(keeper); + strategy.swap(tokens, data, amounts); + } +} diff --git a/test/unit/Withdraw.t.sol b/test/unit/Withdraw.t.sol new file mode 100644 index 0000000..92707da --- /dev/null +++ b/test/unit/Withdraw.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.26; + +import "../ERC4626StrategyTest.t.sol"; + +contract WithdrawTest is ERC4626StrategyTest { + function setUp() public override { + super.setUp(); + + deal(asset, alice, 100e18); + + vm.startPrank(alice); + IERC20(asset).approve(address(strategy), 100e18); + strategy.deposit(100e18, alice); + vm.stopPrank(); + + vm.warp(block.timestamp + 1 weeks); + } + + function test_Withdraw_Normal() public { + uint256 previousBalance = strategy.balanceOf(alice); + uint256 assets = strategy.convertToAssets(previousBalance); + + uint256 totalAssets = strategy.totalAssets(); + uint256 lastTotalAssets = strategy.lastTotalAssets(); + + vm.startPrank(alice); + uint256 previewedWithdraw = strategy.previewWithdraw(assets); + uint256 withdrawed = strategy.withdraw(assets, alice, alice); + vm.stopPrank(); + + uint256 feeShares = strategy.convertToShares( + ((totalAssets - lastTotalAssets) * strategy.performanceFee()) / strategy.BPS() + ); + uint256 developerFeeShares = (feeShares * strategy.developerFee()) / strategy.BPS(); + + assertEq(previewedWithdraw, withdrawed); + assertEq(IERC20(asset).balanceOf(alice), assets); + assertEq(IERC20(asset).balanceOf(address(strategy)), 0); + assertEq(strategy.balanceOf(alice), previousBalance - previewedWithdraw); + assertEq(strategy.balanceOf(strategy.integratorFeeRecipient()), feeShares - developerFeeShares); + assertEq(strategy.balanceOf(strategy.developerFeeRecipient()), developerFeeShares); + } +}