diff --git a/nest/script/DeployNestContracts.s.sol b/nest/script/DeployNestContracts.s.sol index 4a949eb..64e6a6a 100644 --- a/nest/script/DeployNestContracts.s.sol +++ b/nest/script/DeployNestContracts.s.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.25; -import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Script } from "forge-std/Script.sol"; import { Test } from "forge-std/Test.sol"; @@ -13,6 +12,8 @@ import { NestStaking } from "../src/NestStaking.sol"; import { IComponentToken } from "../src/interfaces/IComponentToken.sol"; import { AggregateTokenProxy } from "../src/proxy/AggregateTokenProxy.sol"; import { NestStakingProxy } from "../src/proxy/NestStakingProxy.sol"; + +import { pUSDProxy } from "../src/proxy/pUSDProxy.sol"; import { pUSD } from "../src/token/pUSD.sol"; // Concrete implementation of ComponentToken @@ -43,7 +44,6 @@ contract DeployNestContracts is Script, Test { function run() external { vm.startBroadcast(NEST_ADMIN_ADDRESS); - ERC1967Proxy pUSDProxy = ERC1967Proxy(payable(PUSD_ADDRESS)); AggregateToken aggregateToken = new AggregateToken(); AggregateTokenProxy aggregateTokenProxy = new AggregateTokenProxy( @@ -52,9 +52,9 @@ contract DeployNestContracts is Script, Test { AggregateToken.initialize, ( NEST_ADMIN_ADDRESS, - "Nest Insto Vault", - "NIV", - IComponentToken(address(pUSDProxy)), + "Nest RWA Vault", + "nRWA", + IComponentToken(PUSD_ADDRESS), 1e17, // ask price 1e17 // bid price ) diff --git a/nest/script/UpgradeNestContracts.s.sol b/nest/script/UpgradeNestContracts.s.sol index 382e451..ca1bb8f 100644 --- a/nest/script/UpgradeNestContracts.s.sol +++ b/nest/script/UpgradeNestContracts.s.sol @@ -14,11 +14,13 @@ import { AggregateTokenProxy } from "../src/proxy/AggregateTokenProxy.sol"; contract UpgradeNestContracts is Script, Test { address private constant NEST_ADMIN_ADDRESS = 0xb015762405De8fD24d29A6e0799c12e0Ea81c1Ff; + address private constant BORING_VAULT_ADDRESS = 0xe644F07B1316f28a7F134998e021eA9f7135F351; + UUPSUpgradeable private constant AGGREGATE_TOKEN_PROXY = UUPSUpgradeable(payable(0x659619AEdf381c3739B0375082C2d61eC1fD8835)); // Add the component token addresses - address private constant ASSET_TOKEN = 0xF66DFD0A9304D3D6ba76Ac578c31C84Dc0bd4A00; + address private constant ASSET_TOKEN = 0x2DEc3B6AdFCCC094C31a2DCc83a43b5042220Ea2; // LiquidContinuousMultiTokenVault - Credbull address private constant COMPONENT_TOKEN = 0x4B1fC984F324D2A0fDD5cD83925124b61175f5C6; diff --git a/nest/script/UpgradepUSD.s.sol b/nest/script/UpgradepUSD.s.sol index ce2a86c..f4979c1 100644 --- a/nest/script/UpgradepUSD.s.sol +++ b/nest/script/UpgradepUSD.s.sol @@ -5,7 +5,6 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { ERC1967Utils } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; import { Script } from "forge-std/Script.sol"; -import { Test } from "forge-std/Test.sol"; import { console2 } from "forge-std/console2.sol"; import { pUSDProxy } from "../src/proxy/pUSDProxy.sol"; @@ -13,7 +12,7 @@ import { pUSD } from "../src/token/pUSD.sol"; import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; -contract UpgradePUSD is Script, Test { +contract UpgradePUSD is Script { // Constants address private constant ADMIN_ADDRESS = 0xb015762405De8fD24d29A6e0799c12e0Ea81c1Ff; @@ -35,6 +34,9 @@ contract UpgradePUSD is Script, Test { uint256 public currentTotalSupply; bool public isConnected; + // small hack to be excluded from coverage report + function test() public { } + function setUp() public { // Try to read implementation slot from proxy, this only works with RPC try vm.load(PUSD_PROXY, ERC1967Utils.IMPLEMENTATION_SLOT) returns (bytes32 implementation) { @@ -56,12 +58,13 @@ contract UpgradePUSD is Script, Test { console2.log("Vault:", currentVault); console2.log("Total Supply:", currentTotalSupply); } else { - vm.skip(true); + //TODO: Check this again + vm.skip(false); isConnected = false; } } catch { console2.log("No implementation found - skipping"); - vm.skip(true); + vm.skip(false); isConnected = false; } } diff --git a/nest/src/AggregateToken.sol b/nest/src/AggregateToken.sol index 5d57091..3317c1c 100644 --- a/nest/src/AggregateToken.sol +++ b/nest/src/AggregateToken.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.25; import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; import { ERC1155Holder } from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { StorageSlot } from "@openzeppelin/contracts/utils/StorageSlot.sol"; import { ComponentToken } from "./ComponentToken.sol"; import { IAggregateToken } from "./interfaces/IAggregateToken.sol"; @@ -55,6 +57,9 @@ contract AggregateToken is ComponentToken, IAggregateToken, ERC1155Holder { /// @notice Emitted when the AggregateToken contract is unpaused for deposits event Unpaused(); + /// @notice Emitted when the asset token is updated + event AssetTokenUpdated(IERC20 indexed oldAsset, IERC20 indexed newAsset); + // Errors /** @@ -63,6 +68,9 @@ contract AggregateToken is ComponentToken, IAggregateToken, ERC1155Holder { */ error ComponentTokenAlreadyListed(IComponentToken componentToken); + /// @notice Emitted when a ComponentToken is removed from the component token list + event ComponentTokenRemoved(IComponentToken indexed componentToken); + /** * @notice Indicates a failure because the ComponentToken is not in the component token list * @param componentToken ComponentToken that is not in the component token list @@ -124,7 +132,7 @@ contract AggregateToken is ComponentToken, IAggregateToken, ERC1155Holder { uint256 askPrice, uint256 bidPrice ) public initializer { - super.initialize(owner, name, symbol, IERC20(address(asset_)), false, false); + super.initialize(owner, name, symbol, IERC20(address(asset_)), false, true); AggregateTokenStorage storage $ = _getAggregateTokenStorage(); $.componentTokenList.push(asset_); @@ -166,13 +174,66 @@ contract AggregateToken is ComponentToken, IAggregateToken, ERC1155Holder { uint256 assets, address receiver, address controller - ) public override(ComponentToken, IComponentToken) returns (uint256 shares) { + ) public override(ComponentToken, IComponentToken, ERC4626Upgradeable) returns (uint256 shares) { if (_getAggregateTokenStorage().paused) { revert DepositPaused(); } return super.deposit(assets, receiver, controller); } + /** + * @inheritdoc ERC4626Upgradeable + * @dev Overridden to add pause check before deposit + * @param assets Amount of assets to deposit + * @param receiver Address that will receive the shares + * @return shares Amount of shares minted + */ + function deposit( + uint256 assets, + address receiver + ) public virtual override(ERC4626Upgradeable) returns (uint256 shares) { + if (_getAggregateTokenStorage().paused) { + revert DepositPaused(); + } + return super.deposit(assets, receiver); + } + + /** + * @inheritdoc ComponentToken + * @dev Overridden to add pause check before minting + * @param shares Amount of shares to mint + * @param receiver Address that will receive the shares + * @param controller Address that controls the minting + * @return assets Amount of assets deposited + */ + function mint( + uint256 shares, + address receiver, + address controller + ) public virtual override(ComponentToken) returns (uint256 assets) { + if (_getAggregateTokenStorage().paused) { + revert DepositPaused(); + } + return super.mint(shares, receiver, controller); + } + + /** + * @inheritdoc ERC4626Upgradeable + * @dev Overridden to add pause check before minting + * @param shares Amount of shares to mint + * @param receiver Address that will receive the shares + * @return assets Amount of assets deposited + */ + function mint( + uint256 shares, + address receiver + ) public virtual override(ERC4626Upgradeable) returns (uint256 assets) { + if (_getAggregateTokenStorage().paused) { + revert DepositPaused(); + } + return super.mint(shares, receiver); + } + /// @inheritdoc IComponentToken function redeem( uint256 shares, @@ -223,6 +284,41 @@ contract AggregateToken is ComponentToken, IAggregateToken, ERC1155Holder { emit ComponentTokenListed(componentToken); } + /** + * @notice Remove a ComponentToken from the component token list + * @dev Only the owner can call this function. The ComponentToken must have zero balance to be removed. + * @param componentToken ComponentToken to remove + */ + function removeComponentToken( + IComponentToken componentToken + ) external nonReentrant onlyRole(ADMIN_ROLE) { + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + + // Check if component token exists + if (!$.componentTokenMap[componentToken]) { + revert ComponentTokenNotListed(componentToken); + } + + // Check if it's the current asset + if (address(componentToken) == asset()) { + revert ComponentTokenIsAsset(componentToken); + } + + // Remove from mapping + $.componentTokenMap[componentToken] = false; + + // Remove from array by finding and replacing with last element + for (uint256 i = 0; i < $.componentTokenList.length; i++) { + if ($.componentTokenList[i] == componentToken) { + $.componentTokenList[i] = $.componentTokenList[$.componentTokenList.length - 1]; + $.componentTokenList.pop(); + break; + } + } + + emit ComponentTokenUnlisted(componentToken); + } + /** * @notice Buy ComponentToken using `asset` * @dev Only the owner can call this function, will revert if diff --git a/nest/src/ComponentToken.sol b/nest/src/ComponentToken.sol index b33e647..8aebd78 100644 --- a/nest/src/ComponentToken.sol +++ b/nest/src/ComponentToken.sol @@ -10,7 +10,8 @@ import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/ import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol"; import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; @@ -293,9 +294,7 @@ abstract contract ComponentToken is revert Unimplemented(); } - if (!IERC20(asset()).transferFrom(owner, address(this), assets)) { - revert InsufficientBalance(IERC20(asset()), owner, assets); - } + SafeERC20.safeTransferFrom(IERC20(asset()), owner, address(this), assets); $.pendingDepositRequest[controller] += assets; emit DepositRequest(controller, owner, REQUEST_ID, owner, assets); @@ -359,10 +358,7 @@ abstract contract ComponentToken is $.claimableDepositRequest[controller] = 0; $.sharesDepositRequest[controller] = 0; } else { - // For sync deposits, process normally - if (!IERC20(asset()).transferFrom(controller, address(this), assets)) { - revert InsufficientBalance(IERC20(asset()), controller, assets); - } + SafeERC20.safeTransferFrom(IERC20(asset()), controller, address(this), assets); shares = convertToShares(assets); } @@ -385,23 +381,22 @@ abstract contract ComponentToken is } ComponentTokenStorage storage $ = _getComponentTokenStorage(); - assets = convertToAssets(shares); - if ($.asyncDeposit) { - if ($.claimableDepositRequest[controller] < assets) { - revert InsufficientRequestBalance(controller, assets, 1); + // Check shares directly instead of converting to assets + if ($.sharesDepositRequest[controller] < shares) { + revert InsufficientRequestBalance(controller, shares, 1); } - $.claimableDepositRequest[controller] -= assets; - $.sharesDepositRequest[controller] -= shares; + // Use the pre-calculated assets amount from when deposit was notified + assets = $.claimableDepositRequest[controller]; + $.claimableDepositRequest[controller] = 0; + $.sharesDepositRequest[controller] = 0; } else { - if (!IERC20(asset()).transferFrom(controller, address(this), assets)) { - revert InsufficientBalance(IERC20(asset()), controller, assets); - } + assets = previewMint(shares); + _deposit(msg.sender, receiver, assets, shares); } - _mint(receiver, shares); - - emit Deposit(controller, receiver, assets, shares); + emit Deposit(msg.sender, receiver, assets, shares); + return assets; } /// @inheritdoc IComponentToken @@ -498,9 +493,7 @@ abstract contract ComponentToken is assets = convertToAssets(shares); } - if (!IERC20(asset()).transfer(receiver, assets)) { - revert InsufficientBalance(IERC20(asset()), address(this), assets); - } + SafeERC20.safeTransfer(IERC20(asset()), receiver, assets); emit Withdraw(controller, receiver, controller, assets, shares); return assets; @@ -554,7 +547,7 @@ abstract contract ComponentToken is address receiver, address controller ) public virtual override(ERC4626Upgradeable, IERC7540) nonReentrant returns (uint256 shares) { - if (assets == 0) { + if (shares == 0) { revert ZeroAmount(); } if (msg.sender != controller) { @@ -562,34 +555,20 @@ abstract contract ComponentToken is } ComponentTokenStorage storage $ = _getComponentTokenStorage(); - if ($.asyncRedeem) { - // For async redemptions, we must use the full claimable amount - uint256 claimableShares = $.claimableRedeemRequest[controller]; - uint256 claimableAssets = $.assetsRedeemRequest[controller]; - - if (claimableShares == 0 || claimableAssets == 0) { - revert NoClaimableRedeem(); - } - if (assets != claimableAssets) { - revert InvalidRedeemAmount(convertToShares(assets), claimableShares); + // Use the pre-calculated assets amount from when redeem was notified + if ($.assetsRedeemRequest[controller] < assets) { + revert InsufficientRequestBalance(controller, assets, 3); } - - shares = claimableShares; - - // Reset state atomically + shares = $.claimableRedeemRequest[controller]; $.claimableRedeemRequest[controller] = 0; $.assetsRedeemRequest[controller] = 0; } else { - shares = convertToShares(assets); - _burn(controller, shares); - } - - if (!IERC20(asset()).transfer(receiver, assets)) { - revert InsufficientBalance(IERC20(asset()), address(this), assets); + shares = previewWithdraw(assets); + _withdraw(msg.sender, receiver, msg.sender, assets, shares); } - - emit Withdraw(controller, receiver, controller, assets, shares); + _burn(msg.sender, shares); + emit Withdraw(msg.sender, receiver, msg.sender, assets, shares); return shares; } @@ -649,7 +628,7 @@ abstract contract ComponentToken is if (_getComponentTokenStorage().asyncDeposit) { revert Unimplemented(); } - assets = super.previewDeposit(shares); + assets = convertToAssets(shares); } /** @@ -676,7 +655,7 @@ abstract contract ComponentToken is if (_getComponentTokenStorage().asyncRedeem) { revert Unimplemented(); } - shares = super.previewWithdraw(assets); + shares = convertToShares(assets); } /// @inheritdoc IERC7540 diff --git a/nest/src/token/BoringVaultAdapter.sol b/nest/src/token/BoringVaultAdapter.sol index 2407ad4..845a6c1 100644 --- a/nest/src/token/BoringVaultAdapter.sol +++ b/nest/src/token/BoringVaultAdapter.sol @@ -46,6 +46,7 @@ abstract contract BoringVaultAdapter is error InvalidSender(); error InvalidController(); error InvalidVault(); + error InvalidAccountant(); error AssetNotSupported(); error TellerPaused(); @@ -360,12 +361,18 @@ abstract contract BoringVaultAdapter is */ function previewDeposit( uint256 assets - ) public view virtual override(ComponentToken) returns (uint256) { + ) public view override(ComponentToken) returns (uint256 shares) { BoringVaultAdapterStorage storage $ = _getBoringVaultAdapterStorage(); - return $.boringVault.lens.previewDeposit( - IERC20(address($.asset)), assets, $.boringVault.vault, $.boringVault.accountant - ); + try $.boringVault.vault.decimals() returns (uint8 shareDecimals) { + try $.boringVault.accountant.getRateInQuote(ERC20(asset())) returns (uint256 rate) { + shares = assets.mulDivDown(10 ** shareDecimals, rate); + } catch { + revert InvalidAccountant(); // Or could create a new error like `InvalidAccountant` + } + } catch { + revert InvalidVault(); + } } /** @@ -448,8 +455,7 @@ abstract contract BoringVaultAdapter is function balanceOf( address account ) public view override(IERC20, ERC20Upgradeable) returns (uint256) { - BoringVaultAdapterStorage storage $ = _getBoringVaultAdapterStorage(); - return $.boringVault.lens.balanceOf(account, $.boringVault.vault); + return super.balanceOf(account); } /** @@ -460,8 +466,7 @@ abstract contract BoringVaultAdapter is function assetsOf( address account ) public view virtual override(ComponentToken) returns (uint256) { - BoringVaultAdapterStorage storage $ = _getBoringVaultAdapterStorage(); - return $.boringVault.lens.balanceOfInAssets(account, $.boringVault.vault, $.boringVault.accountant); + return super.assetsOf(account); } // ========== METADATA OVERRIDES ========== diff --git a/nest/test/AggregateToken.t.sol b/nest/test/AggregateToken.t.sol new file mode 100644 index 0000000..8a34ad4 --- /dev/null +++ b/nest/test/AggregateToken.t.sol @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; + +import { AggregateToken } from "../src/AggregateToken.sol"; +import { IComponentToken } from "../src/interfaces/IComponentToken.sol"; +import { AggregateTokenProxy } from "../src/proxy/AggregateTokenProxy.sol"; +import { Test } from "forge-std/Test.sol"; +import { console } from "forge-std/console.sol"; + +import { MockInvalidToken } from "../src/mocks/MockInvalidToken.sol"; +import { MockUSDC } from "../src/mocks/MockUSDC.sol"; + +contract AggregateTokenTest is Test { + + AggregateToken public token; + MockUSDC public usdc; + MockUSDC public newUsdc; + address public owner; + address public user1; + address public user2; + + // Events + event AssetTokenUpdated(IERC20 indexed oldAsset, IERC20 indexed newAsset); + event ComponentTokenListed(IComponentToken indexed componentToken); + event ComponentTokenUnlisted(IComponentToken indexed componentToken); + event ComponentTokenBought( + address indexed buyer, IComponentToken indexed componentToken, uint256 componentTokenAmount, uint256 assets + ); + event ComponentTokenSold( + address indexed seller, IComponentToken indexed componentToken, uint256 componentTokenAmount, uint256 assets + ); + event Paused(); + event Unpaused(); + event ComponentTokenRemoved(IComponentToken indexed componentToken); + + event ComponentTokenBuyRequested( + address indexed buyer, IComponentToken indexed componentToken, uint256 assets, uint256 requestId + ); + + event ComponentTokenSellRequested( + address indexed seller, IComponentToken indexed componentToken, uint256 componentTokenAmount, uint256 requestId + ); + + function setUp() public { + owner = makeAddr("owner"); + user1 = makeAddr("user1"); + user2 = makeAddr("user2"); + + // Deploy tokens + usdc = new MockUSDC(); + newUsdc = new MockUSDC(); + + // Deploy through proxy + AggregateToken impl = new AggregateToken(); + ERC1967Proxy proxy = new AggregateTokenProxy( + address(impl), + abi.encodeCall( + AggregateToken.initialize, + ( + owner, + "Aggregate Token", + "AGG", + IComponentToken(address(usdc)), + 1e18, // 1:1 askPrice + 1e18 // 1:1 bidPrice + ) + ) + ); + token = AggregateToken(address(proxy)); + + // Setup initial balances and approvals + usdc.mint(user1, 1000e6); + vm.prank(user1); + usdc.approve(address(token), type(uint256).max); + } + + function testAddComponentToken() public { + vm.startPrank(owner); + + // Should succeed first time + token.addComponentToken(IComponentToken(address(newUsdc))); + + // Should fail second time + vm.expectRevert( + abi.encodeWithSelector( + AggregateToken.ComponentTokenAlreadyListed.selector, IComponentToken(address(newUsdc)) + ) + ); + token.addComponentToken(IComponentToken(address(newUsdc))); + + vm.stopPrank(); + } + + function testRemoveComponentToken() public { + vm.startPrank(owner); + + // Add a token first + token.addComponentToken(IComponentToken(address(newUsdc))); + + // Should fail when trying to remove current asset + vm.expectRevert( + abi.encodeWithSelector(AggregateToken.ComponentTokenIsAsset.selector, IComponentToken(address(usdc))) + ); + token.removeComponentToken(IComponentToken(address(usdc))); + + // Should succeed with non-asset token + token.removeComponentToken(IComponentToken(address(newUsdc))); + + // Should fail when trying to remove non-existent token + vm.expectRevert( + abi.encodeWithSelector(AggregateToken.ComponentTokenNotListed.selector, IComponentToken(address(newUsdc))) + ); + token.removeComponentToken(IComponentToken(address(newUsdc))); + + vm.stopPrank(); + } + + function testPauseUnpause() public { + vm.startPrank(owner); + + // Should start unpaused + assertFalse(token.isPaused()); + + // Should pause + vm.expectEmit(address(token)); + emit Paused(); + token.pause(); + assertTrue(token.isPaused()); + + // Should fail when already paused + vm.expectRevert(AggregateToken.AlreadyPaused.selector); + token.pause(); + + // Should unpause + vm.expectEmit(address(token)); + emit Unpaused(); + token.unpause(); + assertFalse(token.isPaused()); + + // Should fail when already unpaused + vm.expectRevert(AggregateToken.NotPaused.selector); + token.unpause(); + + vm.stopPrank(); + } + + function testSetPrices() public { + // Grant price updater role + bytes32 priceUpdaterRole = token.PRICE_UPDATER_ROLE(); + vm.startPrank(owner); + token.grantRole(priceUpdaterRole, owner); + + // Test ask price + token.setAskPrice(2e18); + assertEq(token.getAskPrice(), 2e18); + + // Test bid price + token.setBidPrice(1.5e18); + assertEq(token.getBidPrice(), 1.5e18); + + vm.stopPrank(); + } + + function testConversion() public { + // Test convertToShares + assertEq(token.convertToShares(2e18), 2e18); // With askPrice = 1e18, 2 assets = 2 shares + + // Test convertToAssets + assertEq(token.convertToAssets(2e18), 2e18); // With bidPrice = 1e18, 2 shares = 2 assets + + // Test with different prices + vm.startPrank(owner); + token.grantRole(token.PRICE_UPDATER_ROLE(), owner); + token.setAskPrice(2e18); // 2:1 ratio + token.setBidPrice(0.5e18); // 1:2 ratio + vm.stopPrank(); + + assertEq(token.convertToShares(2e18), 1e18); // 2 assets = 1 share at 2:1 ratio + assertEq(token.convertToAssets(2e18), 1e18); // 2 shares = 1 asset at 1:2 ratio + } + + function testDeposit() public { + // Test deposit when paused + vm.prank(owner); + token.pause(); + + vm.expectRevert(AggregateToken.DepositPaused.selector); + token.deposit(1e18, address(this), address(this)); + + // Test successful deposit + vm.prank(owner); + token.unpause(); + + vm.startPrank(user1); + usdc.approve(address(token), 1e18); + uint256 shares = token.deposit(1e18, user1, user1); + assertEq(shares, 1e18); + assertEq(token.balanceOf(user1), 1e18); + vm.stopPrank(); + } + + function testRedeem() public { + // Setup: First deposit some tokens + vm.startPrank(user1); + usdc.approve(address(token), 1e18); + token.deposit(1e18, user1, user1); + + // Test redeem + uint256 assets = token.redeem(1e18, user1, user1); + assertEq(assets, 1e18); + assertEq(token.balanceOf(user1), 0); + assertEq(usdc.balanceOf(user1), 1000e6); // Back to original balance + vm.stopPrank(); + } + + function testTotalAssets() public { + assertEq(token.totalAssets(), 0); + + // Deposit some assets + vm.startPrank(user1); + usdc.approve(address(token), 1e18); + token.deposit(1e18, user1, user1); + assertEq(token.totalAssets(), 1e18); + vm.stopPrank(); + } + + function testApproveComponentToken() public { + vm.startPrank(owner); + + // Test ComponentTokenNotListed error + vm.expectRevert( + abi.encodeWithSelector(AggregateToken.ComponentTokenNotListed.selector, IComponentToken(address(newUsdc))) + ); + token.approveComponentToken(IComponentToken(address(newUsdc)), 1e6); + + // Test successful approval + token.addComponentToken(IComponentToken(address(newUsdc))); + token.approveComponentToken(IComponentToken(address(newUsdc)), 1e6); + assertEq(usdc.allowance(address(token), address(newUsdc)), 1e6); + + vm.stopPrank(); + } + + function testComponentTokenOperations() public { + vm.startPrank(owner); + + // Test buyComponentToken + token.addComponentToken(IComponentToken(address(newUsdc))); + vm.expectEmit(address(token)); + emit ComponentTokenBought(owner, IComponentToken(address(newUsdc)), 1e18, 1e18); + token.buyComponentToken(IComponentToken(address(newUsdc)), 1e18); + + // Test sellComponentToken + vm.expectEmit(address(token)); + emit ComponentTokenSold(owner, IComponentToken(address(newUsdc)), 1e18, 1e18); + token.sellComponentToken(IComponentToken(address(newUsdc)), 1e18); + + // Test requestBuyComponentToken + vm.expectEmit(address(token)); + emit ComponentTokenBuyRequested(owner, IComponentToken(address(newUsdc)), 1e18, 0); + token.requestBuyComponentToken(IComponentToken(address(newUsdc)), 1e18); + + // Test requestSellComponentToken + vm.expectEmit(address(token)); + emit ComponentTokenSellRequested(owner, IComponentToken(address(newUsdc)), 1e18, 0); + token.requestSellComponentToken(IComponentToken(address(newUsdc)), 1e18); + + vm.stopPrank(); + } + + function testGetters() public { + // Test getComponentTokenList + IComponentToken[] memory list = token.getComponentTokenList(); + assertEq(list.length, 1); + assertEq(address(list[0]), address(usdc)); + + // Test getComponentToken + assertTrue(token.getComponentToken(IComponentToken(address(usdc)))); + assertFalse(token.getComponentToken(IComponentToken(address(newUsdc)))); + } + /* + function testSupportsInterface() public { + // Test standard interfaces + assertTrue(token.supportsInterface(type(IERC20).interfaceId)); + assertTrue(token.supportsInterface(type(IERC4626).interfaceId)); + assertTrue(token.supportsInterface(type(IAccessControl).interfaceId)); + + // Test custom interfaces + assertTrue(token.supportsInterface(type(IComponentToken).interfaceId)); + assertTrue(token.supportsInterface(type(IAggregateToken).interfaceId)); + } + */ + // Helper function for access control error message + + function accessControlErrorMessage(address account, bytes32 role) internal pure returns (bytes memory) { + return abi.encodePacked( + "AccessControl: account ", + Strings.toHexString(account), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ); + } + +}