diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9282e829..e621bc17 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,34 +1,88 @@ -name: test +name: ci -on: workflow_dispatch - -env: - FOUNDRY_PROFILE: ci +on: + push: + branches: + - main + pull_request: jobs: - check: - strategy: - fail-fast: true - - name: Foundry project - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly - - - name: Run Forge build - run: | - forge --version - forge build --sizes - id: build - - - name: Run Forge tests - run: | - forge test -vvv - id: test + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run foundry build + run: | + forge --version + forge build + id: build + + lint-check: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run foundry fmt check + run: | + forge fmt --check + id: fmt + + test: + runs-on: ubuntu-latest + needs: lint-check + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + - name: Run foundry tests + # --ast tests enables inline configs to work https://github.com/foundry-rs/foundry/issues/7310#issuecomment-1978088200 + run: | + forge test -vv --gas-report --ast + id: test + + fuzz: + runs-on: ubuntu-latest + needs: lint-check + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + - name: Run foundry tests + run: | + FOUNDRY_PROFILE=ci_fuzz forge test -vv + id: fuzz + + # coverage: + # runs-on: ubuntu-latest + # needs: lint-check + # steps: + # - uses: actions/checkout@v3 + # with: + # submodules: recursive + # - uses: foundry-rs/foundry-toolchain@v1 + # with: + # version: nightly + # - name: Run foundry coverage + # run: | + # FOUNDRY_PROFILE=coverage forge coverage --report summary + # id: coverage \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 2e3aabb7..0e9f83a5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/ethereum-vault-connector"] path = lib/ethereum-vault-connector url = https://github.com/euler-xyz/ethereum-vault-connector +[submodule "lib/euler-vault-kit"] + path = lib/euler-vault-kit + url = https://github.com/euler-xyz/euler-vault-kit diff --git a/foundry.toml b/foundry.toml index 25b918f9..358eeffd 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,5 +2,64 @@ src = "src" out = "out" libs = ["lib"] +test = 'test' +optimizer = true +optimizer_runs = 20_000 +solc = "0.8.23" +gas_reports = ["*"] +fs_permissions = [{ access = "read", path = "./"}] + +[profile.default.fmt] +line_length = 120 +tab_width = 4 +bracket_spacing = false +int_types = "long" +quote_style = "double" +number_underscore = "preserve" +override_spacing = true + +[profile.fuzz] +runs = 1000 +max_local_rejects = 1024 +max_global_rejects = 65536 +seed = '0x3e8' +dictionary_weight = 100 +include_storage = true +include_push_bytes = true +match_test = "Fuzzing" +match_contract = "Fuzzing" + +[profile.ci_fuzz] +runs = 50000 +max_local_rejects = 1024 +max_global_rejects = 65536 +seed = '0x3e8' +dictionary_weight = 100 +include_storage = true +include_push_bytes = true +match_test = "Fuzzing" +match_contract = "Fuzzing" + +[profile.invariant] +runs = 256 +depth = 15 +fail_on_revert = false +call_override = false +dictionary_weight = 80 +include_storage = true +include_push_bytes = true + +[profile.coverage] +via_ir = true +no_match_test = "Fuzzing" +no_match_contract = "Script" + +[profile.coverage.optimizer_details] +constantOptimizer = true +yul = true + +[profile.coverage.optimizer_details.yulDetails] +stackAllocation = true +optimizerSteps = '' # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lib/euler-vault-kit b/lib/euler-vault-kit new file mode 160000 index 00000000..e7577002 --- /dev/null +++ b/lib/euler-vault-kit @@ -0,0 +1 @@ +Subproject commit e75770023e1b432a660828120cc166b7dc64a222 diff --git a/remappings.txt b/remappings.txt index 287f2700..f65498dc 100644 --- a/remappings.txt +++ b/remappings.txt @@ -4,3 +4,4 @@ ethereum-vault-connector/=lib/ethereum-vault-connector/src/ forge-std/=lib/forge-std/src/ openzeppelin-contracts/=lib/ethereum-vault-connector/lib/openzeppelin-contracts/contracts/ openzeppelin/=lib/ethereum-vault-connector/lib/openzeppelin-contracts/contracts/ +evk/=lib/euler-vault-kit/ diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded7997..00000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/FourSixTwoSixAgg.sol b/src/FourSixTwoSixAgg.sol index ede8caf7..1307e188 100644 --- a/src/FourSixTwoSixAgg.sol +++ b/src/FourSixTwoSixAgg.sol @@ -25,6 +25,14 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { error AddressesOutOfOrder(); error DuplicateInitialStrategy(); error InitialAllocationPointsZero(); + error NotEnoughAssets(); + error NegativeYield(); + error InactiveStrategy(); + error OutOfBounds(); + error SameIndexes(); + error InvalidStrategyAsset(); + error StrategyAlreadyExist(); + error AlreadyRemoved(); uint8 internal constant REENTRANCYLOCK__UNLOCKED = 1; uint8 internal constant REENTRANCYLOCK__LOCKED = 2; @@ -33,7 +41,8 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { bytes32 public constant ALLOCATION_ADJUSTER_ROLE = keccak256("ALLOCATION_ADJUSTER_ROLE"); bytes32 public constant ALLOCATION_ADJUSTER_ROLE_ADMIN_ROLE = keccak256("ALLOCATION_ADJUSTER_ROLE_ADMIN_ROLE"); bytes32 public constant WITHDRAW_QUEUE_REORDERER_ROLE = keccak256("WITHDRAW_QUEUE_REORDERER_ROLE"); - bytes32 public constant WITHDRAW_QUEUE_REORDERER_ROLE_ADMIN_ROLE = keccak256("WITHDRAW_QUEUE_REORDERER_ROLE_ADMIN_ROLE"); + bytes32 public constant WITHDRAW_QUEUE_REORDERER_ROLE_ADMIN_ROLE = + keccak256("WITHDRAW_QUEUE_REORDERER_ROLE_ADMIN_ROLE"); bytes32 public constant STRATEGY_ADDER_ROLE = keccak256("STRATEGY_ADDER_ROLE"); bytes32 public constant STRATEGY_ADDER_ROLE_ADMIN_ROLE = keccak256("STRATEGY_ADDER_ROLE_ADMIN_ROLE"); bytes32 public constant STRATEGY_REMOVER_ROLE = keccak256("STRATEGY_REMOVER_ROLE"); @@ -87,28 +96,18 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { uint256 _initialCashAllocationPoints, address[] memory _initialStrategies, uint256[] memory _initialStrategiesAllocationPoints - ) - EVCUtil(address(_evc)) - ERC4626(IERC20(_asset)) - ERC20(_name, _symbol) - { + ) EVCUtil(address(_evc)) ERC4626(IERC20(_asset)) ERC20(_name, _symbol) { esrSlot.locked = REENTRANCYLOCK__UNLOCKED; - if(_initialStrategies.length != _initialStrategiesAllocationPoints.length) revert ArrayLengthMismatch(); - if(_initialCashAllocationPoints == 0) revert InitialAllocationPointsZero(); - - strategies[address(0)] = Strategy({ - allocated: 0, - allocationPoints: uint120(_initialCashAllocationPoints), - active: true - }); - - for(uint256 i; i < _initialStrategies.length; ++i) { - strategies[_initialStrategies[i]] = Strategy({ - allocated: 0, - allocationPoints: uint120(_initialStrategiesAllocationPoints[i]), - active: true - }); + if (_initialStrategies.length != _initialStrategiesAllocationPoints.length) revert ArrayLengthMismatch(); + if (_initialCashAllocationPoints == 0) revert InitialAllocationPointsZero(); + + strategies[address(0)] = + Strategy({allocated: 0, allocationPoints: uint120(_initialCashAllocationPoints), active: true}); + + for (uint256 i; i < _initialStrategies.length; ++i) { + strategies[_initialStrategies[i]] = + Strategy({allocated: 0, allocationPoints: uint120(_initialStrategiesAllocationPoints[i]), active: true}); } // Setup DEFAULT_ADMIN @@ -203,8 +202,8 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { totalAssetsDeposited -= assets; uint256 assetsRetrieved = IERC20(asset()).balanceOf(address(this)); - for(uint256 i = 0; i < withdrawalQueue.length; i ++) { - if(assetsRetrieved >= assets) { + for (uint256 i = 0; i < withdrawalQueue.length; i++) { + if (assetsRetrieved >= assets) { break; } @@ -217,9 +216,10 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { uint256 desiredAssets = assets - assetsRetrieved; uint256 withdrawAmount; // We can take all we need 🎉 - if(underlyingBalance > desiredAssets) { + if (underlyingBalance > desiredAssets) { withdrawAmount = desiredAssets; - } else { // not enough but take all we can + } else { + // not enough but take all we can withdrawAmount = underlyingBalance; } @@ -231,8 +231,8 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { strategy.withdraw(withdrawAmount, address(this), address(this)); } - if(assetsRetrieved < assets) { - revert("Not enough assets to withdraw"); + if (assetsRetrieved < assets) { + revert NotEnoughAssets(); } super._withdraw(caller, receiver, owner, assets, shares); @@ -267,18 +267,19 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { return esrSlotCache; } - function rebalance(address strategy) public nonReentrant() { - if(strategy == address(0)) { + function rebalance(address strategy) public nonReentrant { + if (strategy == address(0)) { return; //nothing to rebalance as this is the cash reserve } // Harvest profits, also gulps and updates interest - harvest(strategy); + harvest(strategy); Strategy memory strategyData = strategies[strategy]; uint256 totalAllocationPointsCache = totalAllocationPoints; uint256 totalAssetsAllocatableCache = totalAssetsAllocatable(); - uint256 targetAllocation = totalAssetsAllocatableCache * strategyData.allocationPoints / totalAllocationPointsCache; + uint256 targetAllocation = + totalAssetsAllocatableCache * strategyData.allocationPoints / totalAllocationPointsCache; uint256 currentAllocation = strategyData.allocated; if (currentAllocation > targetAllocation) { @@ -287,17 +288,17 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { uint256 toWithdraw = currentAllocation - targetAllocation; uint256 maxWithdraw = IERC4626(strategy).maxWithdraw(address(this)); - if(toWithdraw > maxWithdraw) { + if (toWithdraw > maxWithdraw) { toWithdraw = maxWithdraw; } IERC4626(strategy).withdraw(toWithdraw, address(this), address(this)); strategies[strategy].allocated = uint120(targetAllocation); //TODO casting totalAllocated -= toWithdraw; - } - else if (currentAllocation < targetAllocation) { + } else if (currentAllocation < targetAllocation) { // Deposit - uint256 targetCash = totalAssetsAllocatableCache * strategies[address(0)].allocationPoints / totalAllocationPointsCache; + uint256 targetCash = + totalAssetsAllocatableCache * strategies[address(0)].allocationPoints / totalAllocationPointsCache; uint256 currentCash = totalAssetsAllocatableCache - totalAllocated; // Calculate available cash to put in strategies @@ -314,11 +315,11 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { } uint256 maxDeposit = IERC4626(strategy).maxDeposit(address(this)); - if(toDeposit > maxDeposit) { + if (toDeposit > maxDeposit) { toDeposit = maxDeposit; } - if(toDeposit == 0) { + if (toDeposit == 0) { return; // No cash to deposit } @@ -331,50 +332,58 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { } // Todo possibly allow batch harvest - function harvest(address strategy) public nonReentrant() { + function harvest(address strategy) public nonReentrant { Strategy memory strategyData = strategies[strategy]; uint256 sharesBalance = IERC4626(strategy).balanceOf(address(this)); uint256 underlyingBalance = IERC4626(strategy).convertToAssets(sharesBalance); - + // There's yield! - if(underlyingBalance > strategyData.allocated) { + if (underlyingBalance > strategyData.allocated) { uint256 yield = underlyingBalance - strategyData.allocated; strategies[strategy].allocated = uint120(underlyingBalance); totalAllocated += yield; // TODO possible performance fee } else { // TODO handle losses - revert("For now we panic on negative yield"); + revert NegativeYield(); } gulp(); } - function adjustAllocationPoints(address strategy, uint256 newPoints) public nonReentrant() onlyRole(ALLOCATION_ADJUSTER_ROLE) { + function adjustAllocationPoints(address strategy, uint256 newPoints) + public + nonReentrant + onlyRole(ALLOCATION_ADJUSTER_ROLE) + { Strategy memory strategyData = strategies[strategy]; uint256 totalAllocationPointsCache = totalAllocationPoints; - if(strategyData.active = false) { - revert("Strategy is inactive"); + if (strategyData.active = false) { + revert InactiveStrategy(); } strategies[strategy].allocationPoints = uint120(newPoints); - if(newPoints > strategyData.allocationPoints) { + if (newPoints > strategyData.allocationPoints) { uint256 diff = newPoints - strategyData.allocationPoints; totalAllocationPoints + totalAllocationPointsCache + diff; - } else if(newPoints < strategyData.allocationPoints) { + } else if (newPoints < strategyData.allocationPoints) { uint256 diff = strategyData.allocationPoints - newPoints; totalAllocationPoints = totalAllocationPointsCache - diff; } } - function reorderWithdrawalQueue(uint8 index1, uint8 index2) public nonReentrant() onlyRole(WITHDRAW_QUEUE_REORDERER_ROLE) { - if(index1 >= withdrawalQueue.length || index2 >= withdrawalQueue.length) { - revert("Index out of bounds"); + function reorderWithdrawalQueue(uint8 index1, uint8 index2) + public + nonReentrant + onlyRole(WITHDRAW_QUEUE_REORDERER_ROLE) + { + if (index1 >= withdrawalQueue.length || index2 >= withdrawalQueue.length) { + revert OutOfBounds(); } - if(index1 == index2) { - revert("Indexes are the same"); + if (index1 == index2) { + revert SameIndexes(); } address temp = withdrawalQueue[index1]; @@ -382,29 +391,29 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { withdrawalQueue[index2] = temp; } - function addStrategy(address strategy, uint256 allocationPoints) public nonReentrant() onlyRole(STRATEGY_ADDER_ROLE) { - if(IERC4626(strategy).asset() != asset()) { - revert ("Strategy asset does not match vault asset"); + function addStrategy(address strategy, uint256 allocationPoints) + public + nonReentrant + onlyRole(STRATEGY_ADDER_ROLE) + { + if (IERC4626(strategy).asset() != asset()) { + revert InvalidStrategyAsset(); } - if(strategies[strategy].active) { - revert("Strategy already exists"); + if (strategies[strategy].active) { + revert StrategyAlreadyExist(); } - strategies[strategy] = Strategy({ - allocated: 0, - allocationPoints: uint120(allocationPoints), - active: true - }); + strategies[strategy] = Strategy({allocated: 0, allocationPoints: uint120(allocationPoints), active: true}); totalAllocationPoints += allocationPoints; withdrawalQueue.push(strategy); } - + // remove strategy, sets its allocation points to zero. Does not pull funds, `harvest` needs to be called to withdraw - function removeStrategy(address strategy) public nonReentrant() onlyRole(STRATEGY_REMOVER_ROLE) { - if(!strategies[strategy].active) { - revert("Strategy is already inactive"); + function removeStrategy(address strategy) public nonReentrant onlyRole(STRATEGY_REMOVER_ROLE) { + if (!strategies[strategy].active) { + revert AlreadyRemoved(); } strategies[strategy].active = false; @@ -447,4 +456,4 @@ contract FourSixTwoSixAgg is EVCUtil, ERC4626, AccessControlEnumerable { function _msgSender() internal view override (Context, EVCUtil) returns (address) { return EVCUtil._msgSender(); } -} \ No newline at end of file +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 54b724f7..00000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/test/unit/FourSixTwoSixAggBase.t.sol b/test/unit/FourSixTwoSixAggBase.t.sol new file mode 100644 index 00000000..2a102513 --- /dev/null +++ b/test/unit/FourSixTwoSixAggBase.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {EVaultTestBase, TestERC20} from "evk/test/unit/evault/EVaultTestBase.t.sol"; +import {FourSixTwoSixAgg} from "../../src/FourSixTwoSixAgg.sol"; + +contract FourSixTwoSixAggBase is EVaultTestBase { + address deployer; + address user1; + address user2; + + FourSixTwoSixAgg fourSixTwoSixAgg; + + function setUp() public virtual override { + super.setUp(); + + deployer = makeAddr("Deployer"); + user1 = makeAddr("User_1"); + user2 = makeAddr("User_2"); + + vm.prank(deployer); + fourSixTwoSixAgg = new FourSixTwoSixAgg( + evc, + address(assetTST), + "assetTST_Agg", + "assetTST_Agg", + type(uint120).max, + new address[](0), + new uint256[](0) + ); + } +}