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..d782dc6 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_); @@ -223,6 +231,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 46a8bab..e115f2b 100644 --- a/nest/src/ComponentToken.sol +++ b/nest/src/ComponentToken.sol @@ -10,7 +10,6 @@ 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 { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; 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) + ); + } + +}