Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d90dec4
test: Setup and deposit
CheyenneAtapour Dec 3, 2025
c68e67b
test: Basic vault functions
CheyenneAtapour Dec 3, 2025
b37c4b3
test: Deposit with sig
CheyenneAtapour Dec 4, 2025
8b144f0
test: Withdraw w sig
CheyenneAtapour Dec 4, 2025
33ea07d
fix: Non-owner withdraw w sig
CheyenneAtapour Dec 4, 2025
a3cd654
test: Signature functions
CheyenneAtapour Dec 4, 2025
10a15ac
test: Fuzz existing tests
CheyenneAtapour Dec 4, 2025
4f34f40
test: Deploy and deposit fail cases
CheyenneAtapour Dec 4, 2025
2a0b5de
test: Basic unit tests
CheyenneAtapour Dec 4, 2025
1e2450d
fix: Generalize failure cases
CheyenneAtapour Dec 4, 2025
0ded842
fix: Address pr comments
CheyenneAtapour Dec 5, 2025
1fd7c56
chore: cleanup
CheyenneAtapour Dec 5, 2025
fe8af6f
test: Invalid signature cases
CheyenneAtapour Dec 5, 2025
1c2b38f
test: Insufficient allowance tests
CheyenneAtapour Dec 8, 2025
090b396
fix: Pr comments
CheyenneAtapour Dec 9, 2025
0c71ed4
fix: gas snapshot
CheyenneAtapour Dec 9, 2025
d8e2d24
Merge remote-tracking branch 'origin/feat/vault-spoke' into test/vaul…
CheyenneAtapour Dec 15, 2025
72937b3
merge in upstream
CheyenneAtapour Dec 16, 2025
13f4be2
fix: Remove duplicated tests
CheyenneAtapour Dec 16, 2025
8832daa
chore: Try linter
CheyenneAtapour Dec 16, 2025
3e4e65b
chore: lint
CheyenneAtapour Dec 16, 2025
a1a79c3
fix: Pr comments
CheyenneAtapour Dec 16, 2025
8ab9b3e
fix: Remove unused code
CheyenneAtapour Dec 16, 2025
5f1a0c9
Merge remote-tracking branch 'origin/feat/vault-spoke' into test/vaul…
CheyenneAtapour Dec 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions tests/Base.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import {console2 as console} from 'forge-std/console2.sol';

// dependencies
import {AggregatorV3Interface} from 'src/dependencies/chainlink/AggregatorV3Interface.sol';
import {TransparentUpgradeableProxy, ITransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol';
import {
TransparentUpgradeableProxy,
ITransparentUpgradeableProxy
} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol';
import {ProxyAdmin} from 'src/dependencies/openzeppelin/ProxyAdmin.sol';
import {IERC20Metadata} from 'src/dependencies/openzeppelin/IERC20Metadata.sol';
import {SafeCast} from 'src/dependencies/openzeppelin/SafeCast.sol';
Expand Down Expand Up @@ -46,7 +49,11 @@ import {AccessManagerEnumerable} from 'src/access/AccessManagerEnumerable.sol';
import {HubConfigurator, IHubConfigurator} from 'src/hub/HubConfigurator.sol';
import {Hub, IHub, IHubBase} from 'src/hub/Hub.sol';
import {SharesMath} from 'src/hub/libraries/SharesMath.sol';
import {AssetInterestRateStrategy, IAssetInterestRateStrategy, IBasicInterestRateStrategy} from 'src/hub/AssetInterestRateStrategy.sol';
import {
AssetInterestRateStrategy,
IAssetInterestRateStrategy,
IBasicInterestRateStrategy
} from 'src/hub/AssetInterestRateStrategy.sol';

// spoke
import {Spoke, ISpoke, ISpokeBase} from 'src/spoke/Spoke.sol';
Expand Down
22 changes: 22 additions & 0 deletions tests/unit/VaultSpoke/VaultSpoke.Base.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,15 @@ contract VaultSpokeBaseTest is Base {
user: who
});
}

function _deposit(IVaultSpoke vault, address user, uint256 amount) internal {
deal(address(tokenList.dai), user, amount);

vm.startPrank(user);
tokenList.dai.approve(address(vault), amount);
vault.deposit(amount, user);
vm.stopPrank();
}
}

contract VaultSpokeInitTest is VaultSpokeBaseTest {
Expand All @@ -156,6 +165,19 @@ contract VaultSpokeInitTest is VaultSpokeBaseTest {
assertEq(instance.decimals(), hub1.getAsset(assetId).decimals);
}

function test_deploy_reverts_InvalidHub() public {
address invalidHub = address(0);
vm.expectRevert();
new VaultSpokeInstance(invalidHub, daiAssetId);
}

/// @dev Cannot directly initialize the implementation contract
function test_cannot_init_impl() public {
VaultSpokeInstance vaultImpl = new VaultSpokeInstance(address(hub1), daiAssetId);
vm.expectRevert(Initializable.InvalidInitialization.selector);
vaultImpl.initialize('impl name', 'impl symbol');
}

function test_setUp() public {
assertEq(daiVault.name(), SHARE_NAME);
assertEq(daiVault.symbol(), SHARE_SYMBOL);
Expand Down
230 changes: 230 additions & 0 deletions tests/unit/VaultSpoke/VaultSpoke.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2025 Aave Labs
pragma solidity ^0.8.0;

import 'tests/unit/VaultSpoke/VaultSpoke.Base.t.sol';
import {IERC4626} from 'src/dependencies/openzeppelin/IERC4626.sol';

contract VaultSpokeTest is VaultSpokeBaseTest {
function test_deposit(uint256 depositAmount) public {
depositAmount = bound(depositAmount, 1, MAX_SUPPLY_AMOUNT);
deal(address(tokenList.dai), alice, depositAmount);

assertEq(tokenList.dai.balanceOf(alice), depositAmount);
assertEq(tokenList.dai.balanceOf(address(daiVault)), 0);
assertEq(tokenList.dai.balanceOf(address(hub1)), 0);
assertEq(daiVault.balanceOf(alice), 0);

vm.startPrank(alice);
tokenList.dai.approve(address(daiVault), depositAmount);
vm.expectEmit(address(daiVault));
emit IERC4626.Deposit(alice, alice, depositAmount, depositAmount);
uint256 shares = daiVault.deposit(depositAmount, alice);
vm.stopPrank();

assertEq(tokenList.dai.balanceOf(alice), 0);
assertEq(tokenList.dai.balanceOf(address(daiVault)), 0);
assertEq(daiVault.totalAssets(), depositAmount);
assertEq(daiVault.balanceOf(alice), depositAmount);
assertEq(tokenList.dai.balanceOf(address(hub1)), depositAmount);

assertEq(hub1.getSpokeAddedShares(daiAssetId, address(daiVault)), shares);
}

function test_deposit_zero_revertsWith_InvalidAmount() public {
vm.expectRevert(IHub.InvalidAmount.selector);
vm.prank(alice);
daiVault.deposit(0, alice);
}

function test_deposit_revertsWith_MaxDepositExceeded(uint256 newMaxCap) public {
uint256 daiDecimalMultiplier = MathUtils.uncheckedExp(10, tokenList.dai.decimals());
// Max cap is not scaled by asset decimals
newMaxCap = bound(newMaxCap, 1, MAX_SUPPLY_AMOUNT / daiDecimalMultiplier);
uint256 depositAmount = newMaxCap *
daiDecimalMultiplier +
vm.randomUint(1, UINT256_MAX - newMaxCap * daiDecimalMultiplier);
vm.prank(ADMIN);
hub1.updateSpokeConfig(
daiAssetId,
address(daiVault),
IHub.SpokeConfig({
addCap: uint40(newMaxCap),
drawCap: Constants.MAX_ALLOWED_SPOKE_CAP,
riskPremiumThreshold: Constants.MAX_ALLOWED_COLLATERAL_RISK,
active: true,
paused: false
})
);

IHub.SpokeConfig memory config = hub1.getSpokeConfig(daiAssetId, address(daiVault));
assertEq(config.addCap, uint40(newMaxCap));

vm.prank(alice);
tokenList.dai.approve(address(daiVault), depositAmount);
vm.expectRevert(
abi.encodeWithSelector(
IVaultSpoke.MaxDepositExceeded.selector,
newMaxCap * MathUtils.uncheckedExp(10, tokenList.dai.decimals()),
depositAmount
)
);
vm.prank(alice);
daiVault.deposit(depositAmount, alice);
}

function test_mint(uint256 mintAmount) public {
mintAmount = bound(mintAmount, 1, MAX_SUPPLY_AMOUNT);
deal(address(tokenList.dai), alice, mintAmount);

assertEq(tokenList.dai.balanceOf(alice), mintAmount);
assertEq(tokenList.dai.balanceOf(address(daiVault)), 0);
assertEq(tokenList.dai.balanceOf(address(hub1)), 0);
assertEq(daiVault.balanceOf(alice), 0);

vm.startPrank(alice);
tokenList.dai.approve(address(daiVault), mintAmount);
vm.expectEmit(address(daiVault));
emit IERC4626.Deposit(alice, alice, mintAmount, mintAmount);
uint256 shares = daiVault.mint(mintAmount, alice);
vm.stopPrank();

assertEq(tokenList.dai.balanceOf(alice), 0);
assertEq(tokenList.dai.balanceOf(address(daiVault)), 0);
assertEq(daiVault.totalAssets(), mintAmount);
assertEq(daiVault.balanceOf(alice), mintAmount);
assertEq(tokenList.dai.balanceOf(address(hub1)), mintAmount);

assertEq(hub1.getSpokeAddedShares(daiAssetId, address(daiVault)), shares);
}

function test_mint_zero_revertsWith_InvalidAmount() public {
vm.expectRevert(IHub.InvalidAmount.selector);
vm.prank(alice);
daiVault.mint(0, alice);
}

function test_mint_revertsWith_MaxMintExceeded(uint256 newMaxCap) public {
uint256 daiDecimalMultiplier = MathUtils.uncheckedExp(10, tokenList.dai.decimals());
// Max cap is not scaled by asset decimals
newMaxCap = bound(newMaxCap, 1, MAX_SUPPLY_AMOUNT / daiDecimalMultiplier);
uint256 mintAmount = newMaxCap *
daiDecimalMultiplier +
vm.randomUint(1, UINT256_MAX - newMaxCap * daiDecimalMultiplier);
vm.prank(ADMIN);
hub1.updateSpokeConfig(
daiAssetId,
address(daiVault),
IHub.SpokeConfig({
addCap: uint40(newMaxCap),
drawCap: Constants.MAX_ALLOWED_SPOKE_CAP,
riskPremiumThreshold: Constants.MAX_ALLOWED_COLLATERAL_RISK,
active: true,
paused: false
})
);

IHub.SpokeConfig memory config = hub1.getSpokeConfig(daiAssetId, address(daiVault));
assertEq(config.addCap, uint40(newMaxCap));

vm.prank(alice);
tokenList.dai.approve(address(daiVault), mintAmount);
vm.expectRevert(
abi.encodeWithSelector(
IVaultSpoke.MaxMintExceeded.selector,
newMaxCap * MathUtils.uncheckedExp(10, tokenList.dai.decimals()),
mintAmount
)
);
vm.prank(alice);
daiVault.mint(mintAmount, alice);
}

function test_withdraw(uint256 depositAmount) public {
depositAmount = bound(depositAmount, 1, MAX_SUPPLY_AMOUNT);
_deposit(daiVault, alice, depositAmount);

assertEq(daiVault.balanceOf(alice), depositAmount);
assertEq(daiVault.totalAssets(), depositAmount);
assertEq(tokenList.dai.balanceOf(address(hub1)), depositAmount);
assertEq(tokenList.dai.balanceOf(alice), 0);

vm.startPrank(alice);
vm.expectEmit(address(daiVault));
emit IERC4626.Withdraw(alice, alice, alice, depositAmount, depositAmount);
daiVault.withdraw(depositAmount, alice, alice);
vm.stopPrank();

assertEq(daiVault.balanceOf(alice), 0);
assertEq(daiVault.totalAssets(), 0);
assertEq(tokenList.dai.balanceOf(address(hub1)), 0);
assertEq(tokenList.dai.balanceOf(alice), depositAmount);

assertEq(hub1.getSpokeAddedShares(daiAssetId, address(daiVault)), 0);
}

function test_withdraw_revertsWith_InvalidAmount() public {
vm.expectRevert(IHub.InvalidAmount.selector);
vm.prank(alice);
daiVault.withdraw(0, alice, alice);
}

function test_withdraw_revertsWith_MaxWithdrawExceeded(uint256 depositAmount) public {
depositAmount = bound(depositAmount, 1, MAX_SUPPLY_AMOUNT);
uint256 withdrawAmount = depositAmount + vm.randomUint(1, UINT256_MAX - depositAmount);
vm.prank(alice);
_deposit(daiVault, alice, depositAmount);

vm.expectRevert(
abi.encodeWithSelector(
IVaultSpoke.MaxWithdrawExceeded.selector,
depositAmount,
withdrawAmount
)
);
vm.prank(alice);
daiVault.withdraw(withdrawAmount, alice, alice);
}

function test_redeem(uint256 depositAmount) public {
depositAmount = bound(depositAmount, 1, MAX_SUPPLY_AMOUNT);
_deposit(daiVault, alice, depositAmount);

assertEq(daiVault.balanceOf(alice), depositAmount);
assertEq(daiVault.totalAssets(), depositAmount);
assertEq(tokenList.dai.balanceOf(address(hub1)), depositAmount);
assertEq(tokenList.dai.balanceOf(alice), 0);

vm.startPrank(alice);
vm.expectEmit(address(daiVault));
emit IERC4626.Withdraw(alice, alice, alice, depositAmount, depositAmount);
daiVault.redeem(depositAmount, alice, alice);
vm.stopPrank();

assertEq(daiVault.balanceOf(alice), 0);
assertEq(daiVault.totalAssets(), 0);
assertEq(tokenList.dai.balanceOf(address(hub1)), 0);
assertEq(tokenList.dai.balanceOf(alice), depositAmount);

assertEq(hub1.getSpokeAddedShares(daiAssetId, address(daiVault)), 0);
}

function test_redeem_revertsWith_InvalidAmount() public {
vm.expectRevert(IHub.InvalidAmount.selector);
vm.prank(alice);
daiVault.redeem(0, alice, alice);
}

function test_redeem_revertsWith_MaxRedeemExceeded(uint256 depositAmount) public {
depositAmount = bound(depositAmount, 1, MAX_SUPPLY_AMOUNT);
uint256 redeemAmount = depositAmount + vm.randomUint(1, UINT256_MAX - depositAmount);
vm.prank(alice);
_deposit(daiVault, alice, depositAmount);

vm.expectRevert(
abi.encodeWithSelector(IVaultSpoke.MaxRedeemExceeded.selector, depositAmount, redeemAmount)
);
vm.prank(alice);
daiVault.redeem(redeemAmount, alice, alice);
}
}
Loading