Skip to content

Commit 40a133f

Browse files
author
michealking
committed
Initial build: vault, strategies, types, utils, interfaces, mocks (WIP)
1 parent 923a137 commit 40a133f

17 files changed

+1388
-0
lines changed

.github/workflows/test.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
workflow_dispatch:
7+
8+
env:
9+
FOUNDRY_PROFILE: ci
10+
11+
jobs:
12+
check:
13+
name: Foundry project
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
with:
18+
submodules: recursive
19+
20+
- name: Install Foundry
21+
uses: foundry-rs/foundry-toolchain@v1
22+
23+
- name: Show Forge version
24+
run: |
25+
forge --version
26+
27+
- name: Run Forge fmt
28+
run: |
29+
forge fmt --check
30+
id: fmt
31+
32+
- name: Run Forge build
33+
run: |
34+
forge build --sizes
35+
id: build
36+
37+
- name: Run Forge tests
38+
run: |
39+
forge test -vvv
40+
id: test

.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Compiler files
2+
cache/
3+
out/
4+
5+
# Ignores development broadcast logs
6+
!/broadcast
7+
/broadcast/*/31337/
8+
/broadcast/**/dry-run/
9+
10+
# Docs
11+
docs/
12+
13+
# Dotenv file
14+
.env

.gitmodules

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[submodule "lib/forge-std"]
2+
path = lib/forge-std
3+
url = https://github.com/foundry-rs/forge-std
4+
[submodule "lib/buildswithking-security"]
5+
path = lib/buildswithking-security
6+
url = https://github.com/BuildsWithKing/buildswithking-security

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# King Yield Aggregator (KYA)
2+
3+
King Yield Aggregator (KYA) is a modular, role-secured yield vault designed to automatically deploy user deposits into external strategies to earn optimized yield.
4+
It follows a clean separation of concerns across vault logic, strategy management, access control, and safety modules.
5+
6+
## Features
7+
8+
- **Automated Yield Deployment**: Deposits can be routed through a Strategy Manager into yield strategies (e.g., Aave, Compound).
9+
- **Share-Based Accounting**: Users receive vault shares representing their proportional ownership of total assets (on-vault + deployed).
10+
- **Modular Architecture**: Vault, Strategy Manager, and Strategy contracts are separated for extensibility and composability.
11+
- **Secure Access Control**: Powered by the custom Kingable access control suite with pausing, roles, and non-zero address checks.
12+
- **Safe Withdrawals**: Withdrawals pull liquidity from on-vault balance and fallback to strategy withdrawal when needed.
13+
- **Upgradeable Strategies**: Strategy Manager can switch strategies without affecting user deposits or share balances.
14+
15+
## Folder Structure
16+
```
17+
src
18+
├── Rebalancer.sol <-- Coordinate strategy switching & rebalancing
19+
├── StrategyManager.sol <-- Manages strategies, APR updates, lifecycle
20+
├── Types <-- Storage & events for each module
21+
│ ├── ManagerTypes.sol <-- Strategy list, activeStrategyId, etc.
22+
│ ├── StrategyTypes.sol <-- Strategy storage (APR, assets, etc.)
23+
│ └── VaultTypes.sol <-- Shares, admin, vault metadata
24+
├── Utils.sol <-- Custom errors + modifiers (validateAmount)
25+
├── VaultCore.sol <-- Main user-facing vault (deposit/withdraw logic)
26+
├── interfaces
27+
│ └── IStrategy.sol <-- Standard interface for all strategies
28+
└── mocks <-- Aave & Compound mock strategies for testing
29+
├── AaveStrategyMock.sol
30+
└── CompoundStrategyMock.sol
31+
```
32+
33+
## Status
34+
35+
This repository is actively under development as part of a DeFi protocol build.
36+
Code is being pushed progressively while modules, integrations, and tests are still being refined.
37+
38+
## License
39+
40+
MIT License.

foundry.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[profile.default]
2+
src = "src"
3+
out = "out"
4+
libs = ["lib"]
5+
6+
optimizer = true
7+
optimizer_runs = 200
8+
9+
remappings = [
10+
"buildswithking-security/=lib/buildswithking-security/contracts/"
11+
]
12+
13+
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

lib/buildswithking-security

Submodule buildswithking-security added at a5ec1d4

lib/forge-std

Submodule forge-std added at 7117c90

src/Rebalancer.sol

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.30;
3+
4+
/// @title Rebalancer - Rebalancer for KingYieldAggregator.
5+
/// @author Michealking (@BuildsWithKing)
6+
/// @notice Rebalances funds across multiple strategies to maximize APR automatically.
7+
/// @dev Fully integrated with VaultCore and StrategyManager, Uses safe approvals and King-style access control.
8+
9+
/** @notice Created on the 4th of Dec, 2025.
10+
11+
Imports the asset's interface, vaultcore, strategymanager,
12+
kingpausable, shared utilities contracts and non-zero address library.
13+
*/
14+
import {IERC20} from "buildswithking-security/tokens/ERC20/interfaces/IERC20.sol";
15+
import {VaultCore} from "./VaultCore.sol";
16+
import {StrategyManager} from "./StrategyManager.sol";
17+
import {KingPausable} from "buildswithking-security/access/extensions/KingPausable.sol";
18+
import {Utils} from "./Utils.sol";
19+
import {KingCheckAddressLib} from "buildswithking-security/access/utils/KingCheckAddressLib.sol";
20+
21+
contract Rebalancer is Utils, KingPausable {
22+
// ========================================== State Variables ==========================================
23+
/// @notice Records the vault's contract address.
24+
VaultCore public immutable s_vault;
25+
26+
/// @notice Records the strategy manager's contract address.
27+
StrategyManager public immutable s_manager;
28+
29+
/// @notice Records the asset's contract address.
30+
IERC20 public immutable s_asset;
31+
32+
/// @notice Records the admin's address.
33+
address public s_admin;
34+
35+
/// @notice Records the admin's role.
36+
bytes32 public constant REBALANCER_ADMIN = keccak256("REBALANCER_ADMIN");
37+
38+
/// @notice Records the last rebalanced timestamp.
39+
uint256 public lastRebalance;
40+
41+
/// @notice Records the minimum rebalance internal.
42+
uint256 public constant MIN_INTERVAL = 1 hours;
43+
44+
// ========================================= Events ====================================================
45+
/// @notice Emitted once rebalancing is planned.
46+
/// @param fromStrategy The old strategy's address.
47+
/// @param toStrategy The new strategy's address.
48+
event RebalancePlanned(uint256 indexed fromStrategy, uint256 indexed toStrategy);
49+
50+
/// @notice Emitted once rebalancing has occurred.
51+
/// @param fromStrategy The old strategy's address.
52+
/// @param toStrategy The new strategy's address.
53+
/// @param rebalancedAt The rebalanced timestamp.
54+
event Rebalanced(uint256 indexed fromStrategy, uint256 indexed toStrategy, uint256 rebalancedAt);
55+
56+
// =========================================== Constructor ===============================================
57+
/// @notice Sets the king, admin, vault, manager and asset address at deployment.
58+
/// @dev KingAccessControlLite is KingPausable's parent contract. KingCheckAddressLib internally checks for zero admin's address.
59+
/// @param _king The king's address.
60+
/// @param _admin The admin's address.
61+
/// @param _vault The vault's contract address.
62+
/// @param _manager The strategy manager's contract address.
63+
/// @param _asset The asset's contract address.
64+
constructor(address _king, address _admin, address _vault, address _manager, address _asset) KingPausable(_king) {
65+
// Call the internal Library `ensureNonZero` function.
66+
KingCheckAddressLib.ensureNonZero(_vault);
67+
68+
// Call the internal Library `ensureNonZero` function.
69+
KingCheckAddressLib.ensureNonZero(_manager);
70+
71+
// Call the internal Library `ensureNonZero` function.
72+
KingCheckAddressLib.ensureNonZero(_asset);
73+
74+
// Assign the admin.
75+
s_admin = _admin;
76+
77+
// Call KingAccessControlLite internal `_grantRole` function.
78+
_grantRole(REBALANCER_ADMIN, _admin);
79+
80+
// Assign manager.
81+
s_manager = StrategyManager(payable(_manager));
82+
83+
// Assign asset.
84+
s_asset = IERC20(_asset);
85+
86+
// Assign the vault's address.
87+
s_vault = VaultCore(payable(_vault));
88+
}
89+
90+
// ============================================= Internal Helper Functions ===============================
91+
/// @notice Selects the strategy with the highest APR.
92+
/// @return bestId The identification number of the strategy with the highest APR.
93+
function _selectBestStrategy() internal view returns (uint256 bestId) {
94+
// Read all strategies APR.
95+
uint256[] memory aprs = s_manager.strategyAPRs();
96+
97+
// Revert if there's no strategies.
98+
if(aprs.length == 0) {
99+
revert ZeroStrategies();
100+
}
101+
102+
// Assign zero as the best APR.
103+
uint256 bestAPR = 0;
104+
105+
// Loop through all strategies APRs.
106+
for (uint256 i; i < aprs.length;) {
107+
if (aprs[i] > bestAPR) {
108+
bestAPR = aprs[i];
109+
// +1 because strategy manager's indices start at 1.
110+
bestId = i + 1;
111+
}
112+
113+
// Used `unchecked` since overFlow is impossible here.
114+
unchecked {
115+
++i;
116+
}
117+
}
118+
}
119+
120+
/// @notice Ensures there is an active strategy.
121+
function _checkActiveStrategy() internal view {
122+
// Revert if the manager's address is the zero address.
123+
if (s_manager.activeStrategy() == address(0)) {
124+
revert NoActiveStrategy();
125+
}
126+
}
127+
128+
// =================================================== King's External Write Function ========================
129+
/// @notice Assigns the admin's role. Callable only by the king.
130+
/// @dev KingCheckAddressLib internally checks for zero address. Emits the event RoleRevoked & RoleGranted.
131+
/// @param _admin The admin's address.
132+
function assignAdmin(address _admin) external onlyKing {
133+
// Return if the address is the current admin's address.
134+
if (s_admin == _admin) {
135+
return;
136+
}
137+
138+
// Revoke the current admin's role.
139+
_revokeRole(REBALANCER_ADMIN, s_admin);
140+
141+
// Assign the new admin.
142+
s_admin = _admin;
143+
144+
// Call KingAccessControlLite internal `grantRole` function.
145+
_grantRole(REBALANCER_ADMIN, _admin);
146+
}
147+
148+
// ============================================ King and Admin's External Write Function ====================================
149+
/// @notice Performs the rebalance to the strategy with the highest APR.
150+
/**
151+
* @dev Steps:
152+
1. Check last rebalanced timestamp.
153+
* 2. Ensure active strategy exists.
154+
* 3. Read the current strategy's Id.
155+
* 4. Read the best strategy's Id.
156+
* 5. If best strategy is the current strategy, exit.
157+
* 6. Call the `rebalanceFromTo` function in strategy manager.
158+
7. Assign lastRebalance.
159+
*/
160+
function performRebalance() external onlyRole(REBALANCER_ADMIN) nonReentrant {
161+
// Revert if the current time is less than the last rebalanced time plus the minimum interval.
162+
if(block.timestamp < lastRebalance + MIN_INTERVAL) {
163+
revert CooldownIsActive();
164+
}
165+
166+
// Call the internal `_checkActiveStrategy` function.
167+
_checkActiveStrategy();
168+
169+
// Read the current strategy's Id.
170+
uint256 currentStrategyId = s_manager.s_activeStrategyId();
171+
172+
// Read the strategy with the best APR.
173+
uint256 bestStrategyId = _selectBestStrategy();
174+
175+
// Revert if the currentStrategyId is equal to zero.
176+
if(currentStrategyId == 0) {
177+
revert InvalidId(currentStrategyId);
178+
}
179+
180+
// Revert if the bestStrategyId is equal to zero.
181+
if(bestStrategyId == 0) {
182+
revert InvalidId(bestStrategyId);
183+
}
184+
185+
// Return if the bestStrategyId is equal to the currentStrategyId.
186+
if (bestStrategyId == currentStrategyId) {
187+
return;
188+
}
189+
190+
// Emit the event RebalancePlanned.
191+
emit RebalancePlanned(currentStrategyId, bestStrategyId);
192+
193+
// Call the `rebalanceFromTo` function in strategy manager.
194+
s_manager.rebalanceFromTo(currentStrategyId, bestStrategyId);
195+
196+
// Assign the lastRebalance.
197+
lastRebalance = block.timestamp;
198+
199+
// Emit the event Rebalanced.
200+
emit Rebalanced(currentStrategyId, bestStrategyId, lastRebalance);
201+
}
202+
}

0 commit comments

Comments
 (0)