diff --git a/nest/script/DeployNestContracts.s.sol b/nest/script/DeployNestContracts.s.sol index bae24c2..4a949eb 100644 --- a/nest/script/DeployNestContracts.s.sol +++ b/nest/script/DeployNestContracts.s.sol @@ -43,37 +43,8 @@ contract DeployNestContracts is Script, Test { function run() external { vm.startBroadcast(NEST_ADMIN_ADDRESS); - - // Deploy pUSD - /* - pUSD pUSDToken = new pUSD(); - ERC1967Proxy pUSDProxy = - new ERC1967Proxy(address(pUSDToken), abi.encodeCall(pUSD.initialize, (VAULT_ADDRESS, NEST_ADMIN_ADDRESS))); - console2.log("pUSDProxy deployed to:", address(pUSDProxy)); - */ ERC1967Proxy pUSDProxy = ERC1967Proxy(payable(PUSD_ADDRESS)); - // Deploy ConcreteComponentToken - /* - ConcreteComponentToken componentToken = new ConcreteComponentToken(); - ERC1967Proxy componentTokenProxy = new ERC1967Proxy( - address(componentToken), - abi.encodeCall( - ComponentToken.initialize, - ( - NEST_ADMIN_ADDRESS, // owner - "Banana", // name - "BAN", // symbol - IERC20(address(pUSDProxy)), // asset token - false, // async deposit - false // async redeem - ) - ) - ); - console2.log("ComponentTokenProxy deployed to:", address(componentTokenProxy)); - */ - - // Deploy AggregateToken with both component tokens AggregateToken aggregateToken = new AggregateToken(); AggregateTokenProxy aggregateTokenProxy = new AggregateTokenProxy( address(aggregateToken), @@ -91,18 +62,6 @@ contract DeployNestContracts is Script, Test { ); console2.log("AggregateTokenProxy deployed to:", address(aggregateTokenProxy)); - // Add new component tokens - // AggregateToken(address(aggregateTokenProxy)).addComponentToken(IComponentToken(address(pUSDProxy))); - // AggregateToken(address(aggregateTokenProxy)).addComponentToken(IComponentToken(address(componentTokenProxy))); - - // Deploy NestStaking - /* - NestStaking nestStaking = new NestStaking(); - NestStakingProxy nestStakingProxy = - new NestStakingProxy(address(nestStaking), abi.encodeCall(NestStaking.initialize, (NEST_ADMIN_ADDRESS))); - console2.log("NestStakingProxy deployed to:", address(nestStakingProxy)); - */ - vm.stopBroadcast(); } diff --git a/nest/script/DeploypUSD.s.sol b/nest/script/DeploypUSD.s.sol new file mode 100644 index 0000000..ff1ab11 --- /dev/null +++ b/nest/script/DeploypUSD.s.sol @@ -0,0 +1,53 @@ +// 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 { console2 } from "forge-std/console2.sol"; + +import { pUSDProxy } from "../src/proxy/pUSDProxy.sol"; +import { pUSD } from "../src/token/pUSD.sol"; + +contract DeploypUSD is Script { + + address private constant NEST_ADMIN_ADDRESS = 0xb015762405De8fD24d29A6e0799c12e0Ea81c1Ff; + address private constant USDC_ADDRESS = 0x401eCb1D350407f13ba348573E5630B83638E30D; + address private constant USDT_ADDRESS = 0x2413b8C79Ce60045882559f63d308aE3DFE0903d; + + address private constant VAULT_TOKEN = 0xe644F07B1316f28a7F134998e021eA9f7135F351; + address private constant ATOMIC_QUEUE = 0x9fEcc2dFA8B64c27B42757B0B9F725fe881Ddb2a; + address private constant TELLER_ADDRESS = 0xE010B6fdcB0C1A8Bf00699d2002aD31B4bf20B86; + address private constant LENS_ADDRESS = 0x39e4A070c3af7Ea1Cc51377D6790ED09D761d274; + address private constant ACCOUNTANT_ADDRESS = 0x607e6E4dC179Bf754f88094C09d9ee9Af990482a; + + function run() external { + vm.startBroadcast(NEST_ADMIN_ADDRESS); + + // Deploy pUSD implementation + pUSD pUSDToken = new pUSD(); + console2.log("pUSD implementation deployed to:", address(pUSDToken)); + + // Deploy pUSD proxy + ERC1967Proxy pUSDProxyContract = new ERC1967Proxy( + address(pUSDToken), + abi.encodeCall( + pUSD.initialize, + ( + NEST_ADMIN_ADDRESS, + IERC20(USDC_ADDRESS), + IERC20(USDT_ADDRESS), + address(VAULT_TOKEN), + TELLER_ADDRESS, + ATOMIC_QUEUE, + LENS_ADDRESS, + ACCOUNTANT_ADDRESS + ) + ) + ); + console2.log("pUSD proxy deployed to:", address(pUSDProxyContract)); + + vm.stopBroadcast(); + } + +} diff --git a/nest/script/UpgradeNestContracts.s.sol b/nest/script/UpgradeNestContracts.s.sol index 88b502f..75b8496 100644 --- a/nest/script/UpgradeNestContracts.s.sol +++ b/nest/script/UpgradeNestContracts.s.sol @@ -7,6 +7,8 @@ import { Test } from "forge-std/Test.sol"; import { console2 } from "forge-std/console2.sol"; import { AggregateToken } from "../src/AggregateToken.sol"; + +import { IComponentToken } from "../src/interfaces/IComponentToken.sol"; import { AggregateTokenProxy } from "../src/proxy/AggregateTokenProxy.sol"; contract UpgradeNestContracts is Script, Test { @@ -15,18 +17,47 @@ contract UpgradeNestContracts is Script, Test { UUPSUpgradeable private constant AGGREGATE_TOKEN_PROXY = UUPSUpgradeable(payable(0x659619AEdf381c3739B0375082C2d61eC1fD8835)); + // Add the component token addresses + address private constant ASSET_TOKEN = 0xF66DFD0A9304D3D6ba76Ac578c31C84Dc0bd4A00; + + // LiquidContinuousMultiTokenVault + address private constant COMPONENT_TOKEN = 0x4B1fC984F324D2A0fDD5cD83925124b61175f5C6; + function test() public { } function run() external { vm.startBroadcast(NEST_ADMIN_ADDRESS); + // Deploy new implementation AggregateToken newAggregateTokenImpl = new AggregateToken(); assertGt(address(newAggregateTokenImpl).code.length, 0, "AggregateToken should be deployed"); console2.log("New AggregateToken Implementation deployed to:", address(newAggregateTokenImpl)); + // Upgrade to new implementation AGGREGATE_TOKEN_PROXY.upgradeToAndCall(address(newAggregateTokenImpl), ""); + // Get the upgraded contract instance + AggregateToken aggregateToken = AggregateToken(address(AGGREGATE_TOKEN_PROXY)); + + // Add component tokens if they're not already in the list + if (!aggregateToken.getComponentToken(IComponentToken(ASSET_TOKEN))) { + aggregateToken.addComponentToken(IComponentToken(ASSET_TOKEN)); + console2.log("Added ASSET_TOKEN to component list"); + } + + if (!aggregateToken.getComponentToken(IComponentToken(COMPONENT_TOKEN))) { + aggregateToken.addComponentToken(IComponentToken(COMPONENT_TOKEN)); + console2.log("Added SECOND_TOKEN to component list"); + } + vm.stopBroadcast(); + + // Verify the component tokens are in the list + IComponentToken[] memory tokens = aggregateToken.getComponentTokenList(); + console2.log("Number of component tokens:", tokens.length); + for (uint256 i = 0; i < tokens.length; i++) { + console2.log("Component token", i, ":", address(tokens[i])); + } } } diff --git a/nest/script/UpgradepUSD.s.sol b/nest/script/UpgradepUSD.s.sol new file mode 100644 index 0000000..17a7847 --- /dev/null +++ b/nest/script/UpgradepUSD.s.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +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"; +import { pUSD } from "../src/token/pUSD.sol"; + +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; + +contract UpgradePUSD is Script, Test { + + // Constants + address private constant ADMIN_ADDRESS = 0xb015762405De8fD24d29A6e0799c12e0Ea81c1Ff; + address private constant PUSD_PROXY = 0x2DEc3B6AdFCCC094C31a2DCc83a43b5042220Ea2; + address private constant USDC_ADDRESS = 0x401eCb1D350407f13ba348573E5630B83638E30D; + address private constant USDT_ADDRESS = 0x2413b8C79Ce60045882559f63d308aE3DFE0903d; + + address private constant VAULT_TOKEN = 0xe644F07B1316f28a7F134998e021eA9f7135F351; + address private constant ATOMIC_QUEUE = 0x9fEcc2dFA8B64c27B42757B0B9F725fe881Ddb2a; + address private constant TELLER_ADDRESS = 0xE010B6fdcB0C1A8Bf00699d2002aD31B4bf20B86; + address private constant LENS_ADDRESS = 0x39e4A070c3af7Ea1Cc51377D6790ED09D761d274; + address private constant ACCOUNTANT_ADDRESS = 0x607e6E4dC179Bf754f88094C09d9ee9Af990482a; + + // Current state tracking + pUSD public currentImplementation; + string public currentName; + string public currentSymbol; + uint8 public currentDecimals; + address public currentVault; + uint256 public currentTotalSupply; + bool public isConnected; + + 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) { + if (implementation != bytes32(0)) { + address currentImplementationAddr = address(uint160(uint256(implementation))); + console2.log("Found implementation at:", currentImplementationAddr); + isConnected = true; + + currentImplementation = pUSD(PUSD_PROXY); + currentName = currentImplementation.name(); + currentSymbol = currentImplementation.symbol(); + currentDecimals = currentImplementation.decimals(); + currentTotalSupply = currentImplementation.totalSupply(); + + console2.log("Current Implementation State:"); + console2.log("Name:", currentName); + console2.log("Symbol:", currentSymbol); + console2.log("Decimals:", currentDecimals); + console2.log("Vault:", currentVault); + console2.log("Total Supply:", currentTotalSupply); + } else { + vm.assume(true); + isConnected = false; + } + } catch { + console2.log("No implementation found - skipping"); + vm.assume(true); + isConnected = false; + } + } + + function testSimulateUpgrade() public { + // Deploy new implementation in test environment + if (!isConnected) { + vm.assume(true); + } else { + vm.startPrank(ADMIN_ADDRESS); + + pUSD newImplementation = new pUSD(); + UUPSUpgradeable(payable(PUSD_PROXY)).upgradeToAndCall(address(newImplementation), ""); + + pUSD upgradedToken = pUSD(PUSD_PROXY); + + vm.stopPrank(); + console2.log("Upgrade simulation successful"); + } + } + + function run() external { + if (!isConnected) { + vm.assume(true); + } else { + vm.startBroadcast(ADMIN_ADDRESS); + + // Deploy new implementation + pUSD newImplementation = new pUSD(); + console2.log("New Implementation Address:", address(newImplementation)); + + // Get current version + pUSD currentProxy = pUSD(PUSD_PROXY); + uint256 currentVersion = currentProxy.version(); + console2.log("Current Version:", currentVersion); + + // First upgrade the implementation + UUPSUpgradeable(payable(PUSD_PROXY)).upgradeToAndCall( + address(newImplementation), + "" // No initialization data for the upgrade + ); + + // Then call reinitialize separately + pUSD(PUSD_PROXY).reinitialize( + ADMIN_ADDRESS, + IERC20(USDC_ADDRESS), + IERC20(USDT_ADDRESS), + VAULT_TOKEN, + TELLER_ADDRESS, + ATOMIC_QUEUE, + LENS_ADDRESS, + ACCOUNTANT_ADDRESS + ); + + // Verify the upgrade + uint256 newVersion = pUSD(PUSD_PROXY).version(); + + pUSD upgradedToken = pUSD(PUSD_PROXY); + + //require(newVersion == currentVersion + 1, "Version not incremented"); + console2.log("Updated Implementation State:"); + console2.log("Name:", upgradedToken.name()); + console2.log("Symbol:", upgradedToken.symbol()); + console2.log("Decimals:", upgradedToken.decimals()); + console2.log("Vault:", currentVault); + console2.log("Total Supply:", upgradedToken.totalSupply()); + + console2.log("New Version:", newVersion); + + vm.stopBroadcast(); + } + } + +} diff --git a/nest/src/ComponentToken.sol b/nest/src/ComponentToken.sol index af536ef..b8f4001 100644 --- a/nest/src/ComponentToken.sol +++ b/nest/src/ComponentToken.sol @@ -12,6 +12,7 @@ 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"; +import { console } from "forge-std/console.sol"; import { IComponentToken } from "./interfaces/IComponentToken.sol"; import { IERC7540 } from "./interfaces/IERC7540.sol"; @@ -73,6 +74,8 @@ abstract contract ComponentToken is bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); /// @notice Role for the upgrader of the ComponentToken bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + /// @notice Base that is used to divide all price inputs in order to represent e.g. 1.000001 as 1000001e12 + uint256 private constant _BASE = 1e18; // Events @@ -153,7 +156,7 @@ abstract contract ComponentToken is IERC20 asset_, bool asyncDeposit, bool asyncRedeem - ) public initializer { + ) public onlyInitializing { __ERC20_init(name, symbol); __ERC4626_init(asset_); __AccessControl_init(); @@ -177,7 +180,7 @@ abstract contract ComponentToken is */ function _authorizeUpgrade( address newImplementation - ) internal override(UUPSUpgradeable) onlyRole(UPGRADER_ROLE) { } + ) internal virtual override(UUPSUpgradeable) onlyRole(UPGRADER_ROLE) { } /// @inheritdoc IERC165 function supportsInterface( @@ -513,7 +516,8 @@ abstract contract ComponentToken is if (_getComponentTokenStorage().asyncDeposit) { revert Unimplemented(); } - shares = super.previewDeposit(assets); + // Returns how many shares would be minted for given assets + return convertToShares(assets); } /** @@ -539,7 +543,8 @@ abstract contract ComponentToken is if (_getComponentTokenStorage().asyncRedeem) { revert Unimplemented(); } - assets = super.previewRedeem(shares); + // Returns how many assets would be withdrawn for given shares + return convertToAssets(shares); } /** diff --git a/nest/src/interfaces/IAccountantWithRateProviders.sol b/nest/src/interfaces/IAccountantWithRateProviders.sol new file mode 100644 index 0000000..08bb974 --- /dev/null +++ b/nest/src/interfaces/IAccountantWithRateProviders.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { IRateProvider } from "./IRateProvider.sol"; +import { ERC20 } from "@solmate/tokens/ERC20.sol"; + +interface IAccountantWithRateProviders { + + struct AccountantState { + address payoutAddress; + uint128 feesOwedInBase; + uint128 totalSharesLastUpdate; + uint96 exchangeRate; + uint16 allowedExchangeRateChangeUpper; + uint16 allowedExchangeRateChangeLower; + uint64 lastUpdateTimestamp; + bool isPaused; + uint32 minimumUpdateDelayInSeconds; + uint16 managementFee; + } + + struct RateProviderData { + bool isPeggedToBase; + IRateProvider rateProvider; + } + + function accountantState() external view returns (AccountantState calldata); + function rateProviderData( + ERC20 token + ) external view returns (RateProviderData calldata); + + function base() external view returns (ERC20); + function decimals() external view returns (uint8); + function vault() external view returns (address); + + function pause() external; + function unpause() external; + function updateDelay( + uint32 minimumUpdateDelayInSeconds + ) external; + function updateUpper( + uint16 allowedExchangeRateChangeUpper + ) external; + function updateLower( + uint16 allowedExchangeRateChangeLower + ) external; + function updateManagementFee( + uint16 managementFee + ) external; + function updatePayoutAddress( + address payoutAddress + ) external; + function setRateProviderData(ERC20 asset, bool isPeggedToBase, address rateProvider) external; + function updateExchangeRate( + uint96 newExchangeRate + ) external; + function claimFees( + ERC20 feeAsset + ) external; + + function getRate() external view returns (uint256 rate); + function getRateSafe() external view returns (uint256 rate); + function getRateInQuote( + ERC20 quote + ) external view returns (uint256 rateInQuote); + function getRateInQuoteSafe( + ERC20 quote + ) external view returns (uint256 rateInQuote); + + event Paused(); + event Unpaused(); + event DelayInSecondsUpdated(uint32 oldDelay, uint32 newDelay); + event UpperBoundUpdated(uint16 oldBound, uint16 newBound); + event LowerBoundUpdated(uint16 oldBound, uint16 newBound); + event ManagementFeeUpdated(uint16 oldFee, uint16 newFee); + event PayoutAddressUpdated(address oldPayout, address newPayout); + event RateProviderUpdated(address asset, bool isPegged, address rateProvider); + event ExchangeRateUpdated(uint96 oldRate, uint96 newRate, uint64 currentTime); + event FeesClaimed(address indexed feeAsset, uint256 amount); + +} diff --git a/nest/src/interfaces/IAtomicQueue.sol b/nest/src/interfaces/IAtomicQueue.sol new file mode 100644 index 0000000..61bdc45 --- /dev/null +++ b/nest/src/interfaces/IAtomicQueue.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.25; + +import { ERC20 } from "@solmate/tokens/ERC20.sol"; + +/** + * @title IAtomicQueue + * @notice Interface for AtomicQueue contract that allows users to create requests to exchange + * one ERC20 token for another at a specified price. + * @author crispymangoes + */ +interface IAtomicQueue { + + // ========================================= STRUCTS ========================================= + + struct AtomicRequest { + uint64 deadline; + uint88 atomicPrice; + uint96 offerAmount; + bool inSolve; + } + + struct SolveMetaData { + address user; + uint8 flags; + uint256 assetsToOffer; + uint256 assetsForWant; + } + + struct VerboseSolveMetaData { + address user; + bool deadlineExceeded; + bool zeroOfferAmount; + bool insufficientOfferBalance; + bool insufficientOfferAllowance; + uint256 assetsToOffer; + uint256 assetsForWant; + } + + // ========================================= EVENTS ========================================= + + event AtomicRequestUpdated( + address indexed user, + address indexed offerToken, + address indexed wantToken, + uint256 amount, + uint256 deadline, + uint256 minPrice, + uint256 timestamp + ); + + event AtomicRequestFulfilled( + address indexed user, + address indexed offerToken, + address indexed wantToken, + uint256 offerAmountSpent, + uint256 wantAmountReceived, + uint256 timestamp + ); + + event Paused(); + event Unpaused(); + + // ========================================= ERRORS ========================================= + + error AtomicQueue__UserRepeated(address user); + error AtomicQueue__RequestDeadlineExceeded(address user); + error AtomicQueue__UserNotInSolve(address user); + error AtomicQueue__ZeroOfferAmount(address user); + error AtomicQueue__SafeRequestOfferAmountGreaterThanOfferBalance(uint256 offerAmount, uint256 offerBalance); + error AtomicQueue__SafeRequestDeadlineExceeded(uint256 deadline); + error AtomicQueue__SafeRequestInsufficientOfferAllowance(uint256 offerAmount, uint256 offerAllowance); + error AtomicQueue__SafeRequestOfferAmountZero(); + error AtomicQueue__SafeRequestDiscountTooLarge(); + error AtomicQueue__SafeRequestAccountantOfferMismatch(); + error AtomicQueue__SafeRequestCannotCastToUint88(); + error AtomicQueue__Paused(); + + // ========================================= CONSTANTS ========================================= + + function MAX_DISCOUNT() external view returns (uint256); + + // ========================================= STATE VARIABLES ========================================= + + function userAtomicRequest(address user, ERC20 offer, ERC20 want) external view returns (AtomicRequest memory); + function isPaused() external view returns (bool); + + // ========================================= ADMIN FUNCTIONS ========================================= + + function pause() external; + function unpause() external; + + // ========================================= USER FUNCTIONS ========================================= + + function getUserAtomicRequest(address user, ERC20 offer, ERC20 want) external view returns (AtomicRequest memory); + + function isAtomicRequestValid( + ERC20 offer, + address user, + AtomicRequest calldata userRequest + ) external view returns (bool); + + function updateAtomicRequest(ERC20 offer, ERC20 want, AtomicRequest memory userRequest) external; + + // ========================================= SOLVER FUNCTIONS ========================================= + + function solve( + ERC20 offer, + ERC20 want, + address[] calldata users, + bytes calldata runData, + address solver + ) external; + + function viewSolveMetaData( + ERC20 offer, + ERC20 want, + address[] calldata users + ) external view returns (SolveMetaData[] memory metaData, uint256 totalAssetsForWant, uint256 totalAssetsToOffer); + + function viewVerboseSolveMetaData( + ERC20 offer, + ERC20 want, + address[] calldata users + ) + external + view + returns (VerboseSolveMetaData[] memory metaData, uint256 totalAssetsForWant, uint256 totalAssetsToOffer); + +} diff --git a/nest/src/interfaces/IRateProvider.sol b/nest/src/interfaces/IRateProvider.sol new file mode 100644 index 0000000..50fc40f --- /dev/null +++ b/nest/src/interfaces/IRateProvider.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +interface IRateProvider { + + function getRate() external view returns (uint256); + +} diff --git a/nest/src/interfaces/ITeller.sol b/nest/src/interfaces/ITeller.sol new file mode 100644 index 0000000..81084ff --- /dev/null +++ b/nest/src/interfaces/ITeller.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; + +/** + * @title ITeller + * @notice Interface for interacting with the Teller contract which manages vault deposits and withdrawals + */ +interface ITeller { + + // ========== Structs ========== + struct Asset { + bool allowDeposits; + bool allowWithdraws; + uint16 sharePremium; + } + + // ========== Events ========== + event Paused(); + event Unpaused(); + event AssetDataUpdated(address indexed asset, bool allowDeposits, bool allowWithdraws, uint16 sharePremium); + event Deposit( + uint256 indexed nonce, + address indexed receiver, + address indexed depositAsset, + uint256 depositAmount, + uint256 shareAmount, + uint256 depositTimestamp, + uint256 shareLockPeriodAtTimeOfDeposit + ); + event BulkDeposit(address indexed asset, uint256 depositAmount); + event BulkWithdraw(address indexed asset, uint256 shareAmount); + event DepositRefunded(uint256 indexed nonce, bytes32 depositHash, address indexed user); + + // ========== View Functions ========== + + /** + * @notice Check if deposits are currently paused + * @return bool indicating if deposits are paused + */ + function isPaused() external view returns (bool); + + /** + * @notice Check if an asset is supported for deposits/withdrawals + * @param asset The asset to check support for + * @return bool indicating if the asset is supported + */ + function isSupported( + IERC20 asset + ) external view returns (bool); + + /** + * @notice Get asset configuration data + * @param asset The asset to get data for + * @return Asset struct containing configuration data + */ + function assetData( + IERC20 asset + ) external view returns (Asset memory); + + /** + * @notice Get the current share lock period + * @return uint64 The period shares are locked for after deposit + */ + function shareLockPeriod() external view returns (uint64); + + /** + * @notice Get when a user's shares will be unlocked + * @param user The address to check unlock time for + * @return uint256 The timestamp when shares will unlock + */ + function shareUnlockTime( + address user + ) external view returns (uint256); + + // ========== State Changing Functions ========== + + /** + * @notice Deposit assets into the vault + * @param depositAsset The asset being deposited + * @param depositAmount The amount of asset to deposit + * @param minimumMint The minimum amount of shares to receive + * @return shares The amount of shares minted + */ + function deposit( + IERC20 depositAsset, + uint256 depositAmount, + uint256 minimumMint + ) external payable returns (uint256 shares); + + /** + * @notice Deposit assets using permit + * @param depositAsset The asset being deposited + * @param depositAmount The amount of asset to deposit + * @param minimumMint The minimum amount of shares to receive + * @param deadline The deadline for the permit + * @param v v component of signature + * @param r r component of signature + * @param s s component of signature + * @return shares The amount of shares minted + */ + function depositWithPermit( + IERC20 depositAsset, + uint256 depositAmount, + uint256 minimumMint, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256 shares); + + /** + * @notice Bulk deposit function for solvers + * @param depositAsset The asset being deposited + * @param depositAmount The amount of asset to deposit + * @param minimumMint The minimum amount of shares to receive + * @param to The recipient of the shares + * @return shares The amount of shares minted + */ + function bulkDeposit( + IERC20 depositAsset, + uint256 depositAmount, + uint256 minimumMint, + address to + ) external returns (uint256 shares); + + /** + * @notice Bulk withdraw function for solvers + * @param withdrawAsset The asset to receive + * @param shareAmount The amount of shares to burn + * @param minimumAssets The minimum amount of assets to receive + * @param to The recipient of the assets + * @return assetsOut The amount of assets withdrawn + */ + function bulkWithdraw( + IERC20 withdrawAsset, + uint256 shareAmount, + uint256 minimumAssets, + address to + ) external returns (uint256 assetsOut); + + /** + * @notice Refund a deposit during the lock period + * @param nonce The deposit nonce + * @param receiver The original receiver of shares + * @param depositAsset The asset that was deposited + * @param depositAmount The amount that was deposited + * @param shareAmount The amount of shares minted + * @param depositTimestamp When the deposit occurred + * @param shareLockUpPeriodAtTimeOfDeposit The lock period at time of deposit + */ + function refundDeposit( + uint256 nonce, + address receiver, + address depositAsset, + uint256 depositAmount, + uint256 shareAmount, + uint256 depositTimestamp, + uint256 shareLockUpPeriodAtTimeOfDeposit + ) external; + +} diff --git a/nest/src/interfaces/IVault.sol b/nest/src/interfaces/IVault.sol new file mode 100644 index 0000000..5c72471 --- /dev/null +++ b/nest/src/interfaces/IVault.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +interface IVault { + + /** + * @notice Deposits assets into the vault in exchange for shares + * @param from Address providing the assets + * @param asset Token address being deposited + * @param assetAmount Amount of assets to deposit + * @param to Address receiving the vault shares + * @param shareAmount Amount of shares to mint + */ + function enter(address from, address asset, uint256 assetAmount, address to, uint256 shareAmount) external; + + /** + * @notice Withdraws assets from the vault by burning shares + * @param to Address receiving the withdrawn assets + * @param asset Token address being withdrawn + * @param assetAmount Amount of assets to withdraw + * @param from Address providing the shares to burn + * @param shareAmount Amount of shares to burn + */ + function exit(address to, address asset, uint256 assetAmount, address from, uint256 shareAmount) external; + + /** + * @notice Transfers vault shares from one address to another + * @param from Address sending the shares + * @param to Address receiving the shares + * @param amount Number of shares to transfer + * @return bool Success of the transfer operation + */ + function transferFrom(address from, address to, uint256 amount) external returns (bool); + + /** + * @notice Approves another address to spend vault shares + * @param spender Address authorized to spend shares + * @param amount Number of shares approved to spend + * @return bool Success of the approval operation + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @notice Returns the total supply of vault shares + * @return uint256 Total number of shares in existence + */ + function totalSupply() external view returns (uint256); + + /** + * @notice Returns the number of vault shares owned by an account + * @param account Address to check balance for + * @return uint256 Number of shares owned by the account + */ + function balanceOf( + address account + ) external view returns (uint256); + + /** + * @notice Returns the number of decimals used for share precision + * @return uint8 The number of decimals used for share amounts + */ + function decimals() external view returns (uint8); + +} diff --git a/nest/src/interfaces/Ilens.sol b/nest/src/interfaces/Ilens.sol new file mode 100644 index 0000000..ae093c7 --- /dev/null +++ b/nest/src/interfaces/Ilens.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { IAccountantWithRateProviders } from "./IAccountantWithRateProviders.sol"; +import { ITeller } from "./ITeller.sol"; +import { IVault } from "./IVault.sol"; + +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import { ERC20 } from "@solmate/tokens/ERC20.sol"; + +interface ILens { + + function totalAssets( + IVault vault, + IAccountantWithRateProviders accountant + ) external view returns (IERC20 asset, uint256 assets); + + function previewDeposit( + IERC20 depositAsset, + uint256 depositAmount, + IVault vault, + IAccountantWithRateProviders accountant + ) external view returns (uint256 shares); + + function balanceOf(address account, IVault vault) external view returns (uint256 shares); + + function balanceOfInAssets( + address account, + IVault vault, + IAccountantWithRateProviders accountant + ) external view returns (uint256 assets); + + function exchangeRate( + IAccountantWithRateProviders accountant + ) external view returns (uint256 rate); + + function checkUserDeposit( + address account, + IERC20 depositAsset, + uint256 depositAmount, + IVault vault, + ITeller teller + ) external view returns (bool); + +} diff --git a/nest/src/mocks/MockAccountantWithRateProviders.sol b/nest/src/mocks/MockAccountantWithRateProviders.sol new file mode 100644 index 0000000..a32c8c3 --- /dev/null +++ b/nest/src/mocks/MockAccountantWithRateProviders.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { IAccountantWithRateProviders } from "../interfaces/IAccountantWithRateProviders.sol"; + +import { IRateProvider } from "../interfaces/IRateProvider.sol"; +import { ERC20 } from "@solmate/tokens/ERC20.sol"; +import { FixedPointMathLib } from "@solmate/utils/FixedPointMathLib.sol"; + +contract MockAccountantWithRateProviders is IAccountantWithRateProviders { + + using FixedPointMathLib for uint256; + + AccountantState private _accountantState; + mapping(ERC20 => RateProviderData) private _rateProviderData; + + ERC20 public immutable base; + uint8 public immutable decimals; + address public immutable vault; + + constructor(address _vault, address _base, uint96 startingExchangeRate) { + vault = _vault; + base = ERC20(_base); + decimals = ERC20(_base).decimals(); + + _accountantState.exchangeRate = startingExchangeRate; + _accountantState.allowedExchangeRateChangeUpper = 1.1e4; // 110% + _accountantState.allowedExchangeRateChangeLower = 0.9e4; // 90% + _accountantState.minimumUpdateDelayInSeconds = 1 hours; + _accountantState.managementFee = 0.1e4; // 0.1% + } + + function accountantState() external view returns (AccountantState memory) { + return _accountantState; + } + + function rateProviderData( + ERC20 token + ) external view returns (RateProviderData memory) { + return _rateProviderData[token]; + } + + // Admin functions + function pause() external { + _accountantState.isPaused = true; + emit Paused(); + } + + function unpause() external { + _accountantState.isPaused = false; + emit Unpaused(); + } + + function updateDelay( + uint32 minimumUpdateDelayInSeconds + ) external { + uint32 oldDelay = _accountantState.minimumUpdateDelayInSeconds; + _accountantState.minimumUpdateDelayInSeconds = minimumUpdateDelayInSeconds; + emit DelayInSecondsUpdated(oldDelay, minimumUpdateDelayInSeconds); + } + + function updateUpper( + uint16 allowedExchangeRateChangeUpper + ) external { + uint16 oldBound = _accountantState.allowedExchangeRateChangeUpper; + _accountantState.allowedExchangeRateChangeUpper = allowedExchangeRateChangeUpper; + emit UpperBoundUpdated(oldBound, allowedExchangeRateChangeUpper); + } + + function updateLower( + uint16 allowedExchangeRateChangeLower + ) external { + uint16 oldBound = _accountantState.allowedExchangeRateChangeLower; + _accountantState.allowedExchangeRateChangeLower = allowedExchangeRateChangeLower; + emit LowerBoundUpdated(oldBound, allowedExchangeRateChangeLower); + } + + function updateManagementFee( + uint16 managementFee + ) external { + uint16 oldFee = _accountantState.managementFee; + _accountantState.managementFee = managementFee; + emit ManagementFeeUpdated(oldFee, managementFee); + } + + function updatePayoutAddress( + address payoutAddress + ) external { + address oldPayout = _accountantState.payoutAddress; + _accountantState.payoutAddress = payoutAddress; + emit PayoutAddressUpdated(oldPayout, payoutAddress); + } + + // Match the interface signature exactly + function setRateProviderData(ERC20 asset, bool isPeggedToBase, address rateProvider) external override { + _rateProviderData[asset] = + RateProviderData({ isPeggedToBase: isPeggedToBase, rateProvider: IRateProvider(rateProvider) }); + emit RateProviderUpdated(address(asset), isPeggedToBase, rateProvider); + } + + function updateExchangeRate( + uint96 newExchangeRate + ) external { + uint96 oldRate = _accountantState.exchangeRate; + _accountantState.exchangeRate = newExchangeRate; + emit ExchangeRateUpdated(oldRate, newExchangeRate, uint64(block.timestamp)); + } + + function claimFees( + ERC20 feeAsset + ) external { + emit FeesClaimed(address(feeAsset), 0); + } + + // Rate functions + function getRate() external view returns (uint256 rate) { + return _accountantState.exchangeRate; + } + + function getRateSafe() external view returns (uint256 rate) { + require(!_accountantState.isPaused, "Accountant: paused"); + return _accountantState.exchangeRate; + } + + function getRateInQuote( + ERC20 quote + ) external view returns (uint256 rateInQuote) { + if (address(quote) == address(base)) { + return _accountantState.exchangeRate; + } + RateProviderData memory data = _rateProviderData[quote]; + return data.isPeggedToBase ? _accountantState.exchangeRate : data.rateProvider.getRate(); + } + + function getRateInQuoteSafe( + ERC20 quote + ) external view returns (uint256 rateInQuote) { + require(!_accountantState.isPaused, "Accountant: paused"); + return this.getRateInQuote(quote); + } + +} diff --git a/nest/src/mocks/MockAtomicQueue.sol b/nest/src/mocks/MockAtomicQueue.sol new file mode 100644 index 0000000..7b4f262 --- /dev/null +++ b/nest/src/mocks/MockAtomicQueue.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { IAtomicQueue } from "../interfaces/IAtomicQueue.sol"; +import { ERC20 } from "@solmate/tokens/ERC20.sol"; + +contract MockAtomicQueue is IAtomicQueue { + + bool private _paused; + + // State variables + mapping(address => mapping(ERC20 => mapping(ERC20 => AtomicRequest))) private _userAtomicRequest; + uint256 private _mockRate; + uint256 private constant _MAX_DISCOUNT = 0.01e6; + + constructor() { + _mockRate = 1e18; // Default 1:1 rate + } + + // Add missing interface function + function MAX_DISCOUNT() external pure returns (uint256) { + return _MAX_DISCOUNT; + } + + // Admin functions + function setPaused( + bool paused + ) internal { + _paused = paused; + if (paused) { + emit Paused(); + } else { + emit Unpaused(); + } + } + + function userAtomicRequest(address user, ERC20 offer, ERC20 want) external view returns (AtomicRequest memory) { + return _userAtomicRequest[user][offer][want]; + } + + function setMockRate( + uint256 rate + ) external { + _mockRate = rate; + } + + // View functions + function isPaused() external view returns (bool) { + return _paused; + } + + function getRate() external view returns (uint256) { + return _mockRate; + } + + // Core functions + function updateAtomicRequest(ERC20 offer, ERC20 want, AtomicRequest memory userRequest) external { + require(!_paused, "AtomicQueue: paused"); + _userAtomicRequest[msg.sender][offer][want] = userRequest; + emit AtomicRequestUpdated( + msg.sender, + address(offer), + address(want), + userRequest.offerAmount, + userRequest.deadline, + userRequest.atomicPrice, + block.timestamp + ); + } + + function getUserAtomicRequest(address user, ERC20 offer, ERC20 want) external view returns (AtomicRequest memory) { + return _userAtomicRequest[user][offer][want]; + } + + function pause() external { + setPaused(true); + } + + function unpause() external { + setPaused(false); + } + + function isAtomicRequestValid( + ERC20 offer, + address user, + AtomicRequest calldata userRequest + ) external view returns (bool) { + return true; // Mock implementation + } + + function solve( + ERC20 offer, + ERC20 want, + address[] calldata users, + bytes calldata runData, + address solver + ) external { + // Mock implementation + } + + function viewSolveMetaData( + ERC20 offer, + ERC20 want, + address[] calldata users + ) external view returns (SolveMetaData[] memory metaData, uint256 totalAssetsForWant, uint256 totalAssetsToOffer) { + // Mock implementation + } + + function viewVerboseSolveMetaData( + ERC20 offer, + ERC20 want, + address[] calldata users + ) + external + view + returns (VerboseSolveMetaData[] memory metaData, uint256 totalAssetsForWant, uint256 totalAssetsToOffer) + { + // Mock implementation + } + +} diff --git a/nest/src/mocks/MockLens.sol b/nest/src/mocks/MockLens.sol new file mode 100644 index 0000000..d5d9a49 --- /dev/null +++ b/nest/src/mocks/MockLens.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { IAccountantWithRateProviders } from "../interfaces/IAccountantWithRateProviders.sol"; +import { ILens } from "../interfaces/ILens.sol"; +import { ITeller } from "../interfaces/ITeller.sol"; +import { IVault } from "../interfaces/IVault.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { FixedPointMathLib } from "@solmate/utils/FixedPointMathLib.sol"; +import "forge-std/console2.sol"; + +contract MockLens is ILens { + + using FixedPointMathLib for uint256; + + mapping(uint256 => uint256) private previewDepositValues; + mapping(uint256 => uint256) private previewRedeemValues; + mapping(address => uint256) private balances; + mapping(address => mapping(address => uint256)) private vaultBalances; + + function totalAssets( + IVault vault, + IAccountantWithRateProviders accountant + ) external view override returns (IERC20 asset, uint256 assets) { + uint256 totalSupply = vault.totalSupply(); + uint256 rate = accountant.getRate(); + return (IERC20(address(0)), totalSupply.mulDivDown(rate, 10 ** 6)); + } + + function previewDeposit( + IERC20 depositAsset, + uint256 depositAmount, + IVault vault, + IAccountantWithRateProviders accountant + ) external view override returns (uint256 shares) { + // Check if we have a preset value + if (previewDepositValues[depositAmount] != 0) { + return previewDepositValues[depositAmount]; + } + + uint256 rate = accountant.getRate(); + + try vault.decimals() returns (uint8 shareDecimals) { + return depositAmount.mulDivDown(10 ** shareDecimals, rate); + } catch { + // Explicitly revert with InvalidVault error + bytes4 selector = bytes4(keccak256("InvalidVault()")); + assembly { + mstore(0, selector) + revert(0, 4) + } + } + } + + function setPreviewDeposit(uint256 assets, uint256 shares) external { + previewDepositValues[assets] = shares; + } + + function setPreviewRedeem(uint256 shares, uint256 assets) external { + previewRedeemValues[shares] = assets; + } + + function setBalance(address account, uint256 balance) external { + balances[account] = balance; + } + + function balanceOf(address account, IVault vault) external view override returns (uint256) { + // First check if we have a preset balance + if (balances[account] != 0) { + return balances[account]; + } + // Otherwise return the vault balance + return vault.balanceOf(account); + } + + function balanceOfInAssets( + address account, + IVault vault, + IAccountantWithRateProviders accountant + ) external view override returns (uint256 assets) { + uint256 shares = vault.balanceOf(account); + uint256 rate = accountant.getRate(); + uint8 shareDecimals = vault.decimals(); + + assets = shares.mulDivDown(rate, 10 ** shareDecimals); + } + + function exchangeRate( + IAccountantWithRateProviders accountant + ) external view override returns (uint256 rate) { + return accountant.getRate(); + } + + function checkUserDeposit( + address account, + IERC20 depositAsset, + uint256 depositAmount, + IVault vault, + ITeller teller + ) external view override returns (bool) { + if (depositAsset.balanceOf(account) < depositAmount) { + return false; + } + if (depositAsset.allowance(account, address(vault)) < depositAmount) { + return false; + } + if (teller.isPaused()) { + return false; + } + if (!teller.isSupported(depositAsset)) { + return false; + } + return true; + } + +} diff --git a/nest/src/mocks/MockTeller.sol b/nest/src/mocks/MockTeller.sol new file mode 100644 index 0000000..e1dc532 --- /dev/null +++ b/nest/src/mocks/MockTeller.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { ITeller } from "../interfaces/ITeller.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract MockTeller is ITeller { + + bool private _paused; + mapping(IERC20 => bool) private _supportedAssets; + + function setPaused( + bool paused + ) external { + _paused = paused; + } + + function setAssetSupport(IERC20 asset, bool supported) external { + _supportedAssets[asset] = supported; + } + + function isPaused() external view returns (bool) { + return _paused; + } + + function isSupported( + IERC20 asset + ) external view returns (bool) { + return _supportedAssets[asset]; + } + + function deposit( + IERC20 depositAsset, + uint256 depositAmount, + uint256 minimumMint + ) external payable returns (uint256) { + return depositAmount; + } + + function assetData( + IERC20 asset + ) external view returns (Asset memory) { + return + Asset({ allowDeposits: _supportedAssets[asset], allowWithdraws: _supportedAssets[asset], sharePremium: 0 }); + } + + function shareLockPeriod() external view returns (uint64) { + return 0; + } + + function shareUnlockTime( + address user + ) external view returns (uint256) { + return 0; + } + + function depositWithPermit( + IERC20 depositAsset, + uint256 depositAmount, + uint256 minimumMint, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (uint256) { + return depositAmount; + } + + function bulkDeposit( + IERC20 depositAsset, + uint256 depositAmount, + uint256 minimumMint, + address to + ) external returns (uint256) { + return depositAmount; + } + + function bulkWithdraw( + IERC20 withdrawAsset, + uint256 shareAmount, + uint256 minimumAssets, + address to + ) external returns (uint256) { + return shareAmount; + } + + function refundDeposit( + uint256 nonce, + address receiver, + address depositAsset, + uint256 depositAmount, + uint256 shareAmount, + uint256 depositTimestamp, + uint256 shareLockUpPeriodAtTimeOfDeposit + ) external { } + +} diff --git a/nest/src/mocks/MockUSDC.sol b/nest/src/mocks/MockUSDC.sol new file mode 100644 index 0000000..f036ad0 --- /dev/null +++ b/nest/src/mocks/MockUSDC.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockUSDC is ERC20 { + + constructor() ERC20("USD Coin", "USDC") { + _mint(msg.sender, 1_000_000 * 10 ** 6); + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } + + function decimals() public pure override returns (uint8) { + return 6; + } + +} diff --git a/nest/src/mocks/MockVault.sol b/nest/src/mocks/MockVault.sol new file mode 100644 index 0000000..4cf30c2 --- /dev/null +++ b/nest/src/mocks/MockVault.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract MockVault { + + using SafeERC20 for IERC20; + + // token => account => balance + mapping(address => mapping(address => uint256)) private _balances; + + mapping(address => mapping(address => mapping(address => uint256))) private _allowances; + + IERC20 public asset; + IERC20 public immutable usdc; + IERC20 public immutable usdt; + address public beforeTransferHook; + + constructor(address _usdc, address _usdt) { + usdc = IERC20(_usdc); + usdt = IERC20(_usdt); + } + + function enter(address from, address asset_, uint256 assetAmount, address to, uint256 shareAmount) external { + if (assetAmount > 0) { + IERC20(asset_).safeTransferFrom(from, address(this), assetAmount); + } + _balances[asset_][to] += shareAmount; + _allowances[asset_][to][msg.sender] = type(uint256).max; + } + + function exit(address to, address asset_, uint256 assetAmount, address from, uint256 shareAmount) external { + // Change from checking 'from' balance to checking the actual owner's balance + address owner = from == msg.sender ? to : from; + require(_balances[asset_][owner] >= shareAmount, "MockVault: insufficient balance"); + + uint256 allowed = _allowances[asset_][owner][msg.sender]; + if (allowed != type(uint256).max) { + require(allowed >= shareAmount, "MockVault: insufficient allowance"); + _allowances[asset_][owner][msg.sender] = allowed - shareAmount; + } + + _balances[asset_][owner] -= shareAmount; + + // Changed: Transfer to 'to' instead of msg.sender, and always transfer if we have shares + if (shareAmount > 0) { + IERC20(asset_).safeTransfer(to, shareAmount); + } + } + + function transferFrom(address asset_, address from, address to, uint256 amount) external returns (bool) { + require(_balances[asset_][from] >= amount, "MockVault: insufficient balance"); + + uint256 allowed = _allowances[asset_][from][msg.sender]; + if (allowed != type(uint256).max) { + require(allowed >= amount, "MockVault: insufficient allowance"); + _allowances[asset_][from][msg.sender] = allowed - amount; + } + + _balances[asset_][from] -= amount; + _balances[asset_][to] += amount; + return true; + } + + function approve(address asset_, address spender, uint256 amount) external returns (bool) { + _allowances[asset_][msg.sender][spender] = amount; + return true; + } + + function balanceOf( + address account + ) external view returns (uint256) { + // Return total balance across all assets + return _balances[address(usdc)][account] + _balances[address(usdt)][account]; + } + + function totalSupply() external view returns (uint256) { + // Return total supply across all assets + return _balances[address(usdc)][address(this)] + _balances[address(usdt)][address(this)]; + } + + function decimals() external pure returns (uint8) { + return 6; + } + + function tokenBalance(address token, address account) external view returns (uint256) { + return _balances[token][account]; + } + + function setBalance(address token, uint256 amount) external { + _balances[token][address(this)] = amount; + } + + function allowance(address asset_, address owner, address spender) external view returns (uint256) { + return _allowances[asset_][owner][spender]; + } + + function setBeforeTransferHook( + address hook + ) external { + beforeTransferHook = hook; + } + +} diff --git a/nest/src/proxy/pUSDProxy.sol b/nest/src/proxy/pUSDProxy.sol new file mode 100644 index 0000000..0523da0 --- /dev/null +++ b/nest/src/proxy/pUSDProxy.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +/** + * @title pUSDProxy + * @author Eugene Y. Q. Shen + * @notice Proxy contract for pUSD + */ +contract pUSDProxy is ERC1967Proxy { + + /// @notice Indicates a failure because transferring ETH to the proxy is unsupported + error ETHTransferUnsupported(); + + /// @notice Name of the proxy, used to ensure each named proxy has unique bytecode + bytes32 public constant PROXY_NAME = keccak256("pUSDProxy"); + + constructor(address logic, bytes memory data) ERC1967Proxy(logic, data) { } + + /// @dev Fallback function to silence compiler warnings + receive() external payable { + revert ETHTransferUnsupported(); + } + +} diff --git a/nest/src/token/pUSD.sol b/nest/src/token/pUSD.sol index a7c6626..2383e9b 100644 --- a/nest/src/token/pUSD.sol +++ b/nest/src/token/pUSD.sol @@ -6,137 +6,493 @@ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/I import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; +import { ERC20 } from "@solmate/tokens/ERC20.sol"; -import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; +import { IComponentToken } from "../interfaces/IComponentToken.sol"; -interface IVault { +import { IAccountantWithRateProviders } from "../interfaces/IAccountantWithRateProviders.sol"; +import { IAtomicQueue } from "../interfaces/IAtomicQueue.sol"; - function enter(address from, address asset, uint256 assetAmount, address to, uint256 shareAmount) external; - function exit(address to, address asset, uint256 assetAmount, address from, uint256 shareAmount) external; - function transferFrom(address from, address to, uint256 amount) external returns (bool); - function approve(address spender, uint256 amount) external returns (bool); - function balanceOf( - address account - ) external view returns (uint256); +import { ILens } from "../interfaces/ILens.sol"; +import { ITeller } from "../interfaces/ITeller.sol"; +import { IVault } from "../interfaces/IVault.sol"; +import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol"; +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol"; -} +import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +import { ComponentToken } from "../ComponentToken.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { FixedPointMathLib } from "@solmate/utils/FixedPointMathLib.sol"; + +// TODO: REMOVE in production +import "forge-std/console2.sol"; /** * @title pUSD * @author Eugene Y. Q. Shen, Alp Guneysel * @notice Unified Plume USD stablecoin */ -contract pUSD is Initializable, ERC20Upgradeable, AccessControlUpgradeable, UUPSUpgradeable { +contract pUSD is + Initializable, + ERC20Upgradeable, + AccessControlUpgradeable, + UUPSUpgradeable, + ComponentToken, + ReentrancyGuardUpgradeable +{ - using SafeTransferLib for ERC20; + using SafeERC20 for IERC20; + //using Math for uint256; + using FixedPointMathLib for uint256; - // ========== ROLES ========== - bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); - bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); - bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); - bytes32 public constant VAULT_ADMIN_ROLE = keccak256("VAULT_ADMIN_ROLE"); - bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + // ========== ERRORS ========== + error ZeroAddress(); + + error InvalidAsset(); + error InvalidReceiver(); + error InvalidSender(); + error InvalidController(); + error InvalidVault(); + + error AssetNotSupported(); + error TellerPaused(); + + // ========== STORAGE ========== + + struct BoringVault { + ITeller teller; + IVault vault; + IAtomicQueue atomicQueue; + ILens lens; + IAccountantWithRateProviders accountant; + } + + /// @custom:storage-location erc7201:plume.storage.pUSD + struct pUSDStorage { + BoringVault boringVault; + uint8 tokenDecimals; + string tokenName; + string tokenSymbol; + uint256 version; + IERC20 usdc; + IERC20 usdt; + } - // ========== STATE VARIABLES ========== - IVault public vault; - bool public paused; + // keccak256(abi.encode(uint256(keccak256("plume.storage.pUSD")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant PUSD_STORAGE_LOCATION = 0x54ae4f9578cdf7faaee986bff2a08b358f01b852b4da3af4f67309dae312ee00; + + function _getpUSDStorage() private pure returns (pUSDStorage storage $) { + bytes32 position = PUSD_STORAGE_LOCATION; + assembly { + $.slot := position + } + } // ========== EVENTS ========== event VaultChanged(address oldVault, address newVault); - event Paused(address account); - event Unpaused(address account); + event Reinitialized(uint256 version); - // ========== MODIFIERS ========== - modifier whenNotPaused() { - require(!paused, "PUSD: paused"); - _; - } + // ========== ROLES ========== + bytes32 public constant VAULT_ADMIN_ROLE = keccak256("VAULT_ADMIN_ROLE"); - // ========== CONSTRUCTOR & INITIALIZER ========== - /// @custom:oz-upgrades-unsafe-allow constructor + /** + * @notice Prevent the implementation contract from being initialized or reinitialized + * @custom:oz-upgrades-unsafe-allow constructor + */ constructor() { _disableInitializers(); } - function initialize(address _vault, address admin) external initializer { - __ERC20_init("", ""); // Empty strings since we override name() and symbol() - __AccessControl_init(); + /** + * @notice Initialize pUSD + * @param owner Address of the owner of pUSD + * @param usdc_ Address of the underlying asset + * @param usdt_ Address of the underlying asset + * @param vault_ Address of the Boring Vault + * @param atomicQueue_ Address of the AtomicQueue + */ + // + function initialize( + address owner, + IERC20 usdc_, + IERC20 usdt_, + address vault_, + address teller_, + address atomicQueue_, + address lens_, + address accountant_ + ) public initializer { + require(owner != address(0), "Zero address owner"); + require(address(usdc_) != address(0), "Zero address asset"); + require(address(usdt_) != address(0), "Zero address asset"); + + require(vault_ != address(0), "Zero address vault"); + require(teller_ != address(0), "Zero address teller"); + require(atomicQueue_ != address(0), "Zero address AtomicQueue"); + + // Validate asset interface support + try IERC20Metadata(address(usdc_)).decimals() returns (uint8) { } + catch { + revert InvalidAsset(); + } + __UUPSUpgradeable_init(); + __AccessControl_init(); + __ERC20_init("Plume USD", "pUSD"); + __ReentrancyGuard_init(); + + super.initialize(owner, "Plume USD", "pUSD", usdc_, false, false); - vault = IVault(_vault); + pUSDStorage storage $ = _getpUSDStorage(); + $.boringVault.teller = ITeller(teller_); + $.boringVault.vault = IVault(vault_); + $.boringVault.atomicQueue = IAtomicQueue(atomicQueue_); + $.boringVault.lens = ILens(lens_); + $.boringVault.accountant = IAccountantWithRateProviders(accountant_); + $.usdc = usdc_; + $.usdt = usdt_; - _grantRole(DEFAULT_ADMIN_ROLE, admin); - _grantRole(UPGRADER_ROLE, admin); - _grantRole(MINTER_ROLE, admin); - _grantRole(BURNER_ROLE, admin); - _grantRole(VAULT_ADMIN_ROLE, admin); - _grantRole(PAUSER_ROLE, admin); + $.version = 1; // Set initial version + + _grantRole(VAULT_ADMIN_ROLE, owner); + _grantRole(DEFAULT_ADMIN_ROLE, owner); + _grantRole(UPGRADER_ROLE, owner); // Grant upgrader role to owner } - // ========== METADATA OVERRIDES ========== - function decimals() public pure override returns (uint8) { - return 6; + function reinitialize( + address owner, + IERC20 usdc_, + IERC20 usdt_, + address vault_, + address teller_, + address atomicQueue_, + address lens_, + address accountant_ + ) public onlyRole(UPGRADER_ROLE) { + // Reinitialize as needed + require(owner != address(0), "Zero address owner"); + require(address(usdc_) != address(0), "Zero address asset"); + + require(vault_ != address(0), "Zero address vault"); + require(teller_ != address(0), "Zero address teller"); + require(atomicQueue_ != address(0), "Zero address AtomicQueue"); + + pUSDStorage storage $ = _getpUSDStorage(); + + // Increment version + $.version += 1; + $.boringVault.teller = ITeller(teller_); + $.boringVault.vault = IVault(vault_); + $.boringVault.atomicQueue = IAtomicQueue(atomicQueue_); + $.boringVault.lens = ILens(lens_); + $.boringVault.accountant = IAccountantWithRateProviders(accountant_); + + _grantRole(VAULT_ADMIN_ROLE, owner); + _grantRole(DEFAULT_ADMIN_ROLE, owner); + _grantRole(UPGRADER_ROLE, owner); + + emit Reinitialized($.version); } - function name() public pure override returns (string memory) { - return "Plume USD"; + // ========== ADMIN FUNCTIONS ========== + + /** + * @notice Internal function to authorize an upgrade to a new implementation + * @dev Only callable by addresses with UPGRADER_ROLE + * @param newImplementation Address of the new implementation contract + */ + function _authorizeUpgrade( + address newImplementation + ) internal override(ComponentToken, UUPSUpgradeable) onlyRole(UPGRADER_ROLE) { + super._authorizeUpgrade(newImplementation); // Call ComponentToken's checks } - function symbol() public pure override returns (string memory) { - return "pUSD"; + // ========== VIEW FUNCTIONS ========== + + /** + * @notice Get the current vault address + * @return Address of the current vault + */ + function getVault() external view returns (address) { + return address(_getpUSDStorage().boringVault.vault); } - // ========== ADMIN FUNCTIONS ========== - function setVault( - address newVault - ) external onlyRole(VAULT_ADMIN_ROLE) { - address oldVault = address(vault); - vault = IVault(newVault); - emit VaultChanged(oldVault, newVault); + /** + * @notice Get the current teller address + * @return Address of the current teller + */ + function getTeller() external view returns (address) { + return address(_getpUSDStorage().boringVault.teller); } - function pause() external onlyRole(PAUSER_ROLE) { - paused = true; - emit Paused(msg.sender); + /** + * @notice Get the current AtomicQueue address + * @return Address of the current AtomicQueue + */ + function getAtomicQueue() external view returns (address) { + return address(_getpUSDStorage().boringVault.atomicQueue); } - function unpause() external onlyRole(PAUSER_ROLE) { - paused = false; - emit Unpaused(msg.sender); + /** + * @notice Get the current pUSD version + * @return uint256 version of the pUSD contract + */ + function version() public view returns (uint256) { + return _getpUSDStorage().version; } - // Required override for UUPSUpgradeable - function _authorizeUpgrade( - address newImplementation - ) internal override onlyRole(UPGRADER_ROLE) { } + // ========== COMPONENT TOKEN INTEGRATION ========== - // ========== ERC20 OVERRIDES ========== - function transfer(address to, uint256 amount) public override whenNotPaused returns (bool) { - return vault.transferFrom(msg.sender, to, amount); + /** + * @notice Deposit assets and mint corresponding shares + * @param assets Amount of assets to deposit + * @param receiver Address that will receive the shares + * @param controller Address that will control the shares (unused in this implementation) + * @return shares Amount of shares minted + */ + function deposit( + uint256 assets, + address receiver, + address controller, + uint256 minimumMint + ) public virtual nonReentrant returns (uint256 shares) { + if (receiver == address(0)) { + revert InvalidReceiver(); + } + + ITeller teller = ITeller(address(_getpUSDStorage().boringVault.teller)); + + // Verify deposit is allowed through teller + if (teller.isPaused()) { + revert TellerPaused(); + } + if (!teller.isSupported(IERC20(asset()))) { + revert AssetNotSupported(); + } + + // Transfer assets from sender to this contract first + SafeERC20.safeTransferFrom(IERC20(asset()), msg.sender, address(this), assets); + + // Then approve teller to spend assets using forceApprove + SafeERC20.forceApprove(IERC20(asset()), address(teller), assets); + + // Deposit through teller + shares = teller.deposit( + IERC20(asset()), // depositAsset + assets, // depositAmount + minimumMint // minimumMint + ); + + // Transfer shares to receiver + _mint(receiver, shares); + + emit Deposit(msg.sender, receiver, assets, shares); + return shares; + } + + /** + * @notice Burn shares and withdraw corresponding assets + * @param shares Amount of shares to burn + * @param receiver Address that will receive the assets + * @param controller Address that currently controls the shares + * @return assets Amount of assets withdrawn + */ + function redeem( + uint256 shares, + address receiver, + address controller, + uint256 price, + uint64 deadline + ) public virtual nonReentrant returns (uint256 assets) { + if (receiver == address(0)) { + revert InvalidReceiver(); + } + if (controller == address(0)) { + revert InvalidController(); + } + if (deadline < block.timestamp) { + revert("Deadline expired"); + } + + // Get AtomicQueue from storage + IAtomicQueue queue = _getpUSDStorage().boringVault.atomicQueue; + + // Create AtomicRequest struct + IAtomicQueue.AtomicRequest memory request = IAtomicQueue.AtomicRequest({ + deadline: deadline, + atomicPrice: uint88(price), + offerAmount: uint96(shares), + inSolve: false + }); + + IERC20(address(this)).safeIncreaseAllowance(address(queue), shares); + + // Update atomic request + queue.updateAtomicRequest(ERC20(address(this)), ERC20(asset()), request); + + // Get assets received from vault + assets = shares; // 1:1 ratio for preview to match actual redemption + IERC20(asset()).safeTransfer(receiver, assets); + + emit Withdraw(msg.sender, receiver, controller, assets, shares); + return assets; } - function transferFrom(address from, address to, uint256 amount) public override whenNotPaused returns (bool) { - return vault.transferFrom(from, to, amount); + /** + * @notice Calculate how many shares would be minted for a given amount of assets + * @param assets Amount of assets to deposit + * @return shares Amount of shares that would be minted + */ + function previewDeposit( + uint256 assets + ) public view override returns (uint256) { + pUSDStorage storage $ = _getpUSDStorage(); + + return $.boringVault.lens.previewDeposit( + IERC20(address($.usdc)), assets, $.boringVault.vault, $.boringVault.accountant + ); } - function approve(address spender, uint256 amount) public override whenNotPaused returns (bool) { - bool success = super.approve(spender, amount); - vault.approve(spender, amount); - return success; + /** + * @notice Calculate how many assets would be withdrawn for a given amount of shares + * @param shares Amount of shares to redeem + * @return assets Amount of assets that would be withdrawn + */ + function previewRedeem( + uint256 shares + ) public view virtual override returns (uint256 assets) { + pUSDStorage storage $ = _getpUSDStorage(); + + try $.boringVault.vault.decimals() returns (uint8 shareDecimals) { + assets = shares.mulDivDown($.boringVault.accountant.getRateInQuote(ERC20(asset())), 10 ** shareDecimals); + } catch { + revert InvalidVault(); + } } + /// @inheritdoc ERC4626Upgradeable + function convertToShares( + uint256 assets + ) public view virtual override returns (uint256 shares) { + pUSDStorage storage $ = _getpUSDStorage(); + + try $.boringVault.vault.decimals() returns (uint8 shareDecimals) { + shares = assets.mulDivDown(10 ** shareDecimals, $.boringVault.accountant.getRateInQuote(ERC20(asset()))); + } catch { + revert InvalidVault(); + } + } + + /// @inheritdoc ERC4626Upgradeable + function convertToAssets( + uint256 shares + ) public view virtual override returns (uint256 assets) { + pUSDStorage storage $ = _getpUSDStorage(); + try $.boringVault.vault.decimals() returns (uint8 shareDecimals) { + assets = shares.mulDivDown($.boringVault.accountant.getRateInQuote(ERC20(asset())), 10 ** shareDecimals); + } catch { + revert InvalidVault(); + } + } + // ========== ERC20 OVERRIDES ========== + + /** + * @notice Transfer tokens to a specified address + * @param to Address to transfer tokens to + * @param amount Amount of tokens to transfer + * @return bool indicating whether the transfer was successful + */ + function transfer( + address to, + uint256 amount + ) public virtual override(ERC20Upgradeable, IERC20) nonReentrant returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, amount); + return true; + } + + /** + * @notice Transfer tokens from one address to another + * @param from Address to transfer tokens from + * @param to Address to transfer tokens to + * @param amount Amount of tokens to transfer + * @return bool indicating whether the transfer was successful + */ + function transferFrom( + address from, + address to, + uint256 amount + ) public virtual override(ERC20Upgradeable, IERC20) nonReentrant returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, amount); + _transfer(from, to, amount); + return true; + } + + /** + * @notice Get the balance of shares for an account + * @param account The address to check the balance for + * @return The number of shares owned by the account + */ function balanceOf( address account - ) public view override returns (uint256) { - return vault.balanceOf(account); + ) public view override(IERC20, ERC20Upgradeable) returns (uint256) { + pUSDStorage storage $ = _getpUSDStorage(); + return $.boringVault.lens.balanceOf(account, $.boringVault.vault); + } + + /** + * @notice Get the balance in terms of assets for an account + * @param account The address to check the balance for + * @return The value of shares in terms of assets owned by the account + */ + function assetsOf( + address account + ) public view virtual override(ComponentToken) returns (uint256) { + pUSDStorage storage $ = _getpUSDStorage(); + return $.boringVault.lens.balanceOfInAssets(account, $.boringVault.vault, $.boringVault.accountant); + } + + // ========== METADATA OVERRIDES ========== + + /** + * @notice Get the number of decimals for the token + * @return Number of decimals (6) + */ + function decimals() public pure override(ERC4626Upgradeable, ERC20Upgradeable, IERC20Metadata) returns (uint8) { + return 6; + } + + /** + * @notice Get the name of the token + * @return Name of the token + */ + function name() public pure override(ERC20Upgradeable, IERC20Metadata) returns (string memory) { + return "Plume USD"; + } + + /** + * @notice Get the symbol of the token + * @return Symbol of the token + */ + function symbol() public pure override(ERC20Upgradeable, IERC20Metadata) returns (string memory) { + return "pUSD"; } - // ========== INTERFACE SUPPORT ========== + /** + * @notice Check if the contract supports a given interface + * @param interfaceId Interface identifier to check + * @return bool indicating whether the interface is supported + */ function supportsInterface( bytes4 interfaceId - ) public view override(AccessControlUpgradeable) returns (bool) { - return super.supportsInterface(interfaceId); + ) public view virtual override(AccessControlUpgradeable, ComponentToken) returns (bool) { + return interfaceId == type(IERC20).interfaceId || interfaceId == type(IAccessControl).interfaceId + || super.supportsInterface(interfaceId); } } diff --git a/nest/test/pUSD.t.sol b/nest/test/pUSD.t.sol new file mode 100644 index 0000000..26f8e70 --- /dev/null +++ b/nest/test/pUSD.t.sol @@ -0,0 +1,656 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { IAtomicQueue } from "../src/interfaces/IAtomicQueue.sol"; +import { ITeller } from "../src/interfaces/ITeller.sol"; + +import { MockAtomicQueue } from "../src/mocks/MockAtomicQueue.sol"; +import { MockTeller } from "../src/mocks/MockTeller.sol"; + +import { MockAccountantWithRateProviders } from "../src/mocks/MockAccountantWithRateProviders.sol"; +import { MockLens } from "../src/mocks/MockLens.sol"; +import { MockUSDC } from "../src/mocks/MockUSDC.sol"; +import { MockVault } from "../src/mocks/MockVault.sol"; + +import { pUSD } from "../src/token/pUSD.sol"; + +import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol"; + +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +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 { Test } from "forge-std/Test.sol"; +import { console } from "forge-std/console.sol"; + +// Mock contract for testing invalid asset +contract MockInvalidToken { +// Deliberately missing functions to make it invalid +} + +contract MockInvalidVault { + + // Empty contract that will fail when trying to call decimals() + function decimals() external pure returns (uint8) { + revert(); + } + +} + +contract pUSDTest is Test { + + pUSD public token; + MockUSDC public usdc; + MockUSDC public usdt; // Using MockUSDC for USDT too since they have same interface + MockVault public vault; + MockTeller public mockTeller; + MockAtomicQueue public mockAtomicQueue; + MockLens public mockLens; + MockAccountantWithRateProviders public mockAccountant; + + address public payout_address = vm.addr(7_777_777); + address public owner; + address public user1; + address public user2; + + event VaultChanged(MockVault indexed oldVault, MockVault indexed newVault); + + function setUp() public { + owner = address(this); + user1 = address(0x1); + user2 = address(0x2); + + // Deploy contracts + //asset = new MockUSDC(); + usdc = new MockUSDC(); + usdt = new MockUSDC(); // Deploy USDT mock + + vault = new MockVault(address(usdc), address(usdt)); + mockTeller = new MockTeller(); + mockAtomicQueue = new MockAtomicQueue(); + mockLens = new MockLens(); + + mockAccountant = new MockAccountantWithRateProviders(address(vault), address(usdc), 1e6); + mockTeller.setAssetSupport(IERC20(address(usdc)), true); + mockTeller.setAssetSupport(IERC20(address(usdt)), true); + + // Set the MockTeller as the beforeTransferHook in the vault + vault.setBeforeTransferHook(address(mockTeller)); + + // Deploy through proxy + pUSD impl = new pUSD(); + ERC1967Proxy proxy = new ERC1967Proxy( + address(impl), + abi.encodeCall( + pUSD.initialize, + ( + owner, + IERC20(address(usdc)), + IERC20(address(usdt)), + address(vault), + address(mockTeller), + address(mockAtomicQueue), + address(mockLens), + address(mockAccountant) + ) + ) + ); + token = pUSD(address(proxy)); + + // Setup balances + usdc.mint(user1, 1000e6); + usdt.mint(user1, 1000e6); + vm.prank(user1); + usdc.approve(address(token), type(uint256).max); + usdt.approve(address(token), type(uint256).max); + } + + function testInitialize() public { + assertEq(token.name(), "Plume USD"); + assertEq(token.symbol(), "pUSD"); + assertEq(token.decimals(), 6); + assertTrue(token.hasRole(token.DEFAULT_ADMIN_ROLE(), owner)); + assertTrue(token.hasRole(token.VAULT_ADMIN_ROLE(), owner)); + assertTrue(token.hasRole(token.UPGRADER_ROLE(), owner)); + } + + function testDeposit() public { + uint256 depositAmount = 100e6; + + vm.startPrank(user1); + uint256 shares = token.deposit(depositAmount, user1, user1, 0); + vm.stopPrank(); + + assertEq(shares, depositAmount); // Assuming 1:1 ratio + } + + function testRedeem() public { + uint256 depositAmount = 1e6; + uint256 price = 1e6; // 1:1 price + uint256 minimumMint = depositAmount; + + // Setup + deal(address(usdc), user1, depositAmount); + deal(address(usdt), user1, depositAmount); + + vm.startPrank(user1); + + // Approve all necessary contracts + usdc.approve(address(token), type(uint256).max); + usdc.approve(address(vault), type(uint256).max); + usdc.approve(address(mockTeller), type(uint256).max); + + usdt.approve(address(token), type(uint256).max); + usdt.approve(address(vault), type(uint256).max); + usdt.approve(address(mockTeller), type(uint256).max); + + // Additional approval needed for the vault to transfer from pUSD + vm.stopPrank(); + vm.startPrank(address(token)); + usdc.approve(address(vault), type(uint256).max); + usdt.approve(address(vault), type(uint256).max); + vm.stopPrank(); + + vm.startPrank(user1); + + // Perform deposit and redeem + token.deposit(depositAmount, user1, user1, minimumMint); + + uint64 deadline = uint64(block.timestamp + 1 hours); + token.redeem(depositAmount, user1, user1, price, deadline); + + vm.stopPrank(); + + // TODO: warp time and verify final state + } + + function testInitializeInvalidAsset() public { + // Deploy an invalid token that doesn't implement IERC20Metadata + MockInvalidToken invalidAsset = new MockInvalidToken(); + pUSD impl = new pUSD(); + + bytes memory initData = abi.encodeCall( + pUSD.initialize, + ( + owner, + IERC20(address(invalidAsset)), + IERC20(address(invalidAsset)), + address(vault), + address(mockTeller), + address(mockAtomicQueue), + address(mockLens), + address(mockAccountant) + ) + ); + + vm.expectRevert(pUSD.InvalidAsset.selector); + new ERC1967Proxy(address(impl), initData); + } + + function testReinitialize() public { + // First reinitialize with valid parameters + token.reinitialize( + owner, + IERC20(address(usdc)), + IERC20(address(usdt)), + address(vault), + address(mockTeller), + address(mockAtomicQueue), + address(mockLens), + address(mockAccountant) + ); + + assertNotEq(token.version(), 1); + + // Test zero address requirements + vm.expectRevert("Zero address owner"); + token.reinitialize( + address(0), + IERC20(address(usdc)), + IERC20(address(usdt)), + address(vault), + address(mockTeller), + address(mockAtomicQueue), + address(mockLens), + address(mockAccountant) + ); + + vm.expectRevert("Zero address asset"); + + token.reinitialize( + owner, + IERC20(address(0)), + IERC20(address(usdt)), + address(vault), + address(mockTeller), + address(mockAtomicQueue), + address(mockLens), + address(mockAccountant) + ); + + vm.expectRevert("Zero address vault"); + token.reinitialize( + owner, + IERC20(address(usdc)), + IERC20(address(usdt)), + address(0), + address(mockTeller), + address(mockAtomicQueue), + address(mockLens), + address(mockAccountant) + ); + + vm.expectRevert("Zero address teller"); + token.reinitialize( + owner, + IERC20(address(usdc)), + IERC20(address(usdt)), + address(vault), + address(0), + address(mockAtomicQueue), + address(mockLens), + address(mockAccountant) + ); + + vm.expectRevert("Zero address AtomicQueue"); + token.reinitialize( + owner, + IERC20(address(usdc)), + IERC20(address(usdt)), + address(vault), + address(mockTeller), + address(0), + address(mockLens), + address(mockAccountant) + ); + } + + function testAuthorizeUpgrade() public { + address newImplementation = address(new pUSD()); + + // Test with non-upgrader role + vm.startPrank(user1); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, user1, token.UPGRADER_ROLE() + ) + ); + + token.upgradeToAndCall(newImplementation, ""); + vm.stopPrank(); + + // Test successful upgrade + vm.startPrank(owner); + token.upgradeToAndCall(newImplementation, ""); + vm.stopPrank(); + } + + function testGetters() public { + assertEq(token.getTeller(), address(mockTeller)); + assertEq(token.getAtomicQueue(), address(mockAtomicQueue)); + assertEq(token.version(), 1); + } + + function testDepositReverts() public { + uint256 depositAmount = 100e6; + + // Test invalid receiver + vm.startPrank(user1); + vm.expectRevert(pUSD.InvalidReceiver.selector); + token.deposit(depositAmount, address(0), user1, 0); + vm.stopPrank(); + + // Test teller paused + mockTeller.setPaused(true); + vm.startPrank(user1); + vm.expectRevert(pUSD.TellerPaused.selector); + token.deposit(depositAmount, user1, user1, 0); + vm.stopPrank(); + + // Test asset not supported + mockTeller.setPaused(false); + mockTeller.setAssetSupport(IERC20(address(usdc)), false); + mockTeller.setAssetSupport(IERC20(address(usdt)), false); + vm.startPrank(user1); + vm.expectRevert(pUSD.AssetNotSupported.selector); + token.deposit(depositAmount, user1, user1, 0); + vm.stopPrank(); + } + + function testRedeemReverts() public { + uint256 amount = 100e6; + uint256 price = 1e6; + uint64 deadline = uint64(block.timestamp + 1 hours); + + // Setup + vm.startPrank(user1); + token.deposit(amount, user1, user1, 0); + + // Test invalid receiver + vm.expectRevert(pUSD.InvalidReceiver.selector); + token.redeem(amount, address(0), user1, price, deadline); + + // Test invalid controller + vm.expectRevert(pUSD.InvalidController.selector); + token.redeem(amount, user1, address(0), price, deadline); + + vm.stopPrank(); + } + + function testRedeemDeadlineExpired() public { + uint256 amount = 100e6; + uint256 price = 1e6; + + // Setup + vm.startPrank(user1); + token.deposit(amount, user1, user1, 0); + + // Mock the lens to return correct balance + mockLens.setBalance(user1, amount); + + // Set block.timestamp to a known value + vm.warp(1000); + + // Set deadline in the past + uint64 expiredDeadline = uint64(block.timestamp - 1); + + // Test expired deadline + vm.expectRevert("Deadline expired"); + token.redeem(amount, user1, user1, price, expiredDeadline); + + vm.stopPrank(); + } + + function testPreviewDepositInvalidVault() public { + // Deploy an invalid vault (empty contract) + MockInvalidVault invalidVault = new MockInvalidVault(); + + vm.startPrank(owner); + + // Grant UPGRADER_ROLE to owner for reinitialize + token.grantRole(token.UPGRADER_ROLE(), owner); + + //vm.expectRevert(pUSD.ZeroAddress.selector); + // Reinitialize with the new vault + token.reinitialize( + owner, + IERC20(address(usdc)), + IERC20(address(usdt)), + address(invalidVault), + address(mockTeller), + address(mockAtomicQueue), + address(mockLens), + address(mockAccountant) + ); + + // Now we can test the preview functions with the new vault + vm.expectRevert(pUSD.InvalidVault.selector); + token.previewDeposit(100e6); + + vm.stopPrank(); + } + + function testPreviewRedeemInvalidVault() public { + MockInvalidVault invalidVault = new MockInvalidVault(); + + vm.startPrank(owner); + // Grant UPGRADER_ROLE to owner for reinitialize + token.grantRole(token.UPGRADER_ROLE(), owner); + + // Reinitialize with the new vault + token.reinitialize( + owner, + IERC20(address(usdc)), + IERC20(address(usdt)), + address(invalidVault), + address(mockTeller), + address(mockAtomicQueue), + address(mockLens), + address(mockAccountant) + ); + + // Now we can test the preview functions with the new vault + vm.expectRevert(pUSD.InvalidVault.selector); + token.previewRedeem(100e6); + + vm.stopPrank(); + } + + function testConvertFunctionsAndReverts() public { + uint256 amount = 100e6; + + // Test normal operation first + mockAccountant.updateExchangeRate(2e6); // 2:1 rate + + // With 2:1 rate: + // 100 assets should convert to 50 shares (assets/rate) + uint256 shares = token.convertToShares(amount); + assertEq(shares, amount / 2, "Incorrect shares calculation"); + + // 50 shares should convert to 100 assets (shares*rate) + uint256 assets = token.convertToAssets(shares); + assertEq(assets, amount, "Incorrect assets calculation"); + + // Now test reverts with invalid vault + MockInvalidVault invalidVault = new MockInvalidVault(); + + vm.startPrank(owner); + token.grantRole(token.UPGRADER_ROLE(), owner); + + // Reinitialize with invalid vault + token.reinitialize( + owner, + IERC20(address(usdc)), + IERC20(address(usdt)), + address(invalidVault), + address(mockTeller), + address(mockAtomicQueue), + address(mockLens), + address(mockAccountant) + ); + + // Test convertToShares revert + vm.expectRevert(pUSD.InvalidVault.selector); + token.convertToShares(amount); + + // Test convertToAssets revert + vm.expectRevert(pUSD.InvalidVault.selector); + token.convertToAssets(amount); + + vm.stopPrank(); + } + + function testTransferFrom() public { + uint256 amount = 100e6; + + // Setup initial balance for user1 + vm.startPrank(user1); + token.deposit(amount, user1, user1, 0); + + // Mock the lens to return correct balances + mockLens.setBalance(user1, amount); + + // Approve user2 to spend tokens + token.approve(user2, amount); + vm.stopPrank(); + + // Initial balances + assertEq(token.balanceOf(user1), amount, "Initial balance user1 incorrect"); + assertEq(token.balanceOf(user2), 0, "Initial balance user2 incorrect"); + + // Test transferFrom with user2 + vm.startPrank(user2); + token.transferFrom(user1, user2, amount); + + // Update mock balances after transfer + mockLens.setBalance(user1, 0); + mockLens.setBalance(user2, amount); + + // Check final balances + assertEq(token.balanceOf(user1), 0, "Final balance user1 incorrect"); + assertEq(token.balanceOf(user2), amount, "Final balance user2 incorrect"); + + // Check allowance was spent + assertEq(token.allowance(user1, user2), 0, "Allowance should be spent"); + vm.stopPrank(); + } + + function testConvertToAssets() public { + uint256 shares = 100e6; + uint256 assets = token.convertToAssets(shares); + assertEq(assets, shares); // Assuming 1:1 ratio + } + + function testTransferFunctions() public { + uint256 amount = 100e6; + + // Setup + vm.startPrank(user1); + token.deposit(amount, user1, user1, 0); + + // Set initial balance in MockLens + mockLens.setBalance(user1, amount); + + // Test transfer + token.transfer(user2, amount / 2); + + // Update mock balances after transfer + mockLens.setBalance(user1, amount / 2); + mockLens.setBalance(user2, amount / 2); + + // Verify balances after transfer + assertEq(token.balanceOf(user1), amount / 2, "User1 balance incorrect after transfer"); + assertEq(token.balanceOf(user2), amount / 2, "User2 balance incorrect after transfer"); + + // Test transferFrom + token.approve(user2, amount / 2); + vm.stopPrank(); + + vm.prank(user2); + token.transferFrom(user1, user2, amount / 2); + + // Update mock balances after transferFrom + mockLens.setBalance(user1, 0); + mockLens.setBalance(user2, amount); + + // Verify final balances + assertEq(token.balanceOf(user1), 0, "User1 final balance incorrect"); + assertEq(token.balanceOf(user2), amount, "User2 final balance incorrect"); + } + + // 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) + ); + } + + function testVaultIntegration() public { + uint256 amount = 1e6; + + vm.startPrank(user1); + token.deposit(amount, user1, user1); + vm.stopPrank(); + } + + function testVault() public { + // Verify the vault address matches what we set in setUp + assertEq(address(token.getVault()), address(vault)); + } + + function testSupportsInterface() public { + // Test for ERC20 interface + bytes4 erc20InterfaceId = type(IERC20).interfaceId; + assertTrue(token.supportsInterface(erc20InterfaceId)); + + // Test for AccessControl interface + bytes4 accessControlInterfaceId = type(IAccessControl).interfaceId; + assertTrue(token.supportsInterface(accessControlInterfaceId)); + + // Test for non-supported interface + bytes4 randomInterfaceId = bytes4(keccak256("random()")); + assertFalse(token.supportsInterface(randomInterfaceId)); + } + + function testPreviewDeposit() public { + uint256 depositAmount = 100e6; // 100 USDC + + // Set the exchange rate to 1:1 (1e6) + mockAccountant.updateExchangeRate(1e6); + + // Preview deposit should return same amount as shares (1:1 ratio) + uint256 expectedShares = token.previewDeposit(depositAmount); + assertEq(expectedShares, depositAmount, "Preview deposit amount mismatch"); + + // Verify actual deposit matches preview + vm.startPrank(user1); + uint256 actualShares = token.deposit(depositAmount, user1, user1, 0); + vm.stopPrank(); + + assertEq(actualShares, expectedShares, "Actual shares don't match preview"); + } + + function testPreviewRedeem() public { + uint256 depositAmount = 100e6; + uint256 redeemAmount = 50e6; + uint64 deadline = uint64(block.timestamp + 1 hours); + + // Setup: First deposit some tokens + vm.startPrank(user1); + token.deposit(depositAmount, user1, user1, 0); + + // Preview redeem should return same amount as assets (1:1 ratio) + uint256 expectedAssets = token.previewRedeem(redeemAmount); + assertEq(expectedAssets, redeemAmount); + + // Verify actual redeem matches preview + uint256 actualAssets = token.redeem(redeemAmount, user1, user1, 1e6, deadline); + vm.stopPrank(); + + assertEq(actualAssets, expectedAssets, "Redeem amount doesn't match preview"); + } + + function testBalanceOf() public { + uint256 depositAmount = 100e6; + + // Initial balances should be 0 + assertEq(token.balanceOf(user1), 0, "Initial share balance should be 0"); + assertEq(token.assetsOf(user1), 0, "Initial asset balance should be 0"); + + // Setup initial rate in accountant + mockAccountant.updateExchangeRate(1e6); // 1:1 rate + + vm.startPrank(user1); + + // Approve vault to spend USDC + usdc.approve(address(vault), type(uint256).max); + + // Deposit through vault + vault.enter(user1, address(usdc), depositAmount, user1, depositAmount); + + // Check both balances + assertEq(token.balanceOf(user1), depositAmount, "Share balance after deposit incorrect"); + assertEq(token.assetsOf(user1), depositAmount, "Asset balance after deposit incorrect with 1:1 rate"); + + // Test with different exchange rate + mockAccountant.updateExchangeRate(2e6); // 2:1 rate + + // Share balance should remain the same + assertEq(token.balanceOf(user1), depositAmount, "Share balance should not change with rate"); + // Asset balance should double + assertEq(token.assetsOf(user1), depositAmount * 2, "Asset balance incorrect with 2:1 rate"); + + vm.stopPrank(); + } + + // small hack to be excluded from coverage report + function test() public { } + +} diff --git a/nest/test/pUSDPlume.t.sol b/nest/test/pUSDPlume.t.sol new file mode 100644 index 0000000..eb95efa --- /dev/null +++ b/nest/test/pUSDPlume.t.sol @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { MockVault } from "../src/mocks/MockVault.sol"; +import { pUSD } from "../src/token/pUSD.sol"; + +import { IAtomicQueue } from "../src/interfaces/IAtomicQueue.sol"; +import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol"; + +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +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 { Test } from "forge-std/Test.sol"; +import { console } from "forge-std/console.sol"; + +contract pUSDPlumeTest is Test { + + pUSD public token; + IERC20 public asset; + IERC4626 public vault; + + address public owner; + address public user1; + address public user2; + + // Constants for deployed contracts + address constant USDC_ADDRESS = 0x401eCb1D350407f13ba348573E5630B83638E30D; + address constant VAULT_ADDRESS = 0xe644F07B1316f28a7F134998e021eA9f7135F351; + address constant TELLER_ADDRESS = 0xE010B6fdcB0C1A8Bf00699d2002aD31B4bf20B86; + address constant ATOMIC_QUEUE_ADDRESS = 0x9fEcc2dFA8B64c27B42757B0B9F725fe881Ddb2a; + + address constant PUSD_PROXY = 0x2DEc3B6AdFCCC094C31a2DCc83a43b5042220Ea2; + + event VaultChanged(IERC4626 indexed oldVault, IERC4626 indexed newVault); + + bool private skipTests; + + modifier skipIfNoRPC() { + if (skipTests) { + vm.skip(true); + } else { + _; + } + } + + function setUp() public { + string memory PLUME_RPC = vm.envOr("PLUME_RPC_URL", string("")); + if (bytes(PLUME_RPC).length == 0) { + console.log("PLUME_RPC_URL is not defined"); + skipTests = true; + + // Skip all tests if RPC URL is not defined + vm.skip(false); + return; + } + + vm.createSelectFork(PLUME_RPC); + + // Setup accounts using the private key + uint256 privateKey = 0xf1906c3250e18e8036273019f2d6d4d5107404b84753068fe8fb170674461f1b; + owner = vm.addr(privateKey); + user1 = vm.addr(privateKey); + user2 = address(0x2); + + // Set the default signer for all transactions + vm.startPrank(owner, owner); + + // Connect to deployed contracts + token = pUSD(PUSD_PROXY); + asset = IERC20(USDC_ADDRESS); + vault = IERC4626(VAULT_ADDRESS); + IAtomicQueue atomicQueue = IAtomicQueue(ATOMIC_QUEUE_ADDRESS); + + deal(address(asset), owner, 1000e6); + + // Approve all necessary contracts + asset.approve(address(token), type(uint256).max); + asset.approve(address(vault), type(uint256).max); + asset.approve(TELLER_ADDRESS, type(uint256).max); + + /* + // Additional setup for the vault if needed + if (IAccessControl(address(vault)).hasRole(keccak256("APPROVER_ROLE"), owner)) { + vault.approve(address(token), type(uint256).max); + vault.approve(TELLER_ADDRESS, type(uint256).max); + } + */ + + vm.stopPrank(); + } + + function testDeposit() public skipIfNoRPC { + uint256 depositAmount = 1e6; + uint256 minimumMint = depositAmount; + + // Setup + deal(address(asset), user1, depositAmount * 2); + + vm.startPrank(user1); + + // Approve both token and vault + asset.approve(address(token), type(uint256).max); + asset.approve(address(vault), type(uint256).max); + asset.approve(TELLER_ADDRESS, type(uint256).max); + + // Additional approval needed for the vault to transfer from pUSD + vm.stopPrank(); + + // Add approval from pUSD to vault + vm.startPrank(address(token)); + asset.approve(address(vault), type(uint256).max); + vm.stopPrank(); + + vm.startPrank(user1); + + console.log("Asset balance before deposit:", asset.balanceOf(user1)); + console.log("Asset allowance for token:", asset.allowance(user1, address(token))); + console.log("Asset allowance for vault:", asset.allowance(user1, address(vault))); + console.log("Asset allowance for teller:", asset.allowance(user1, TELLER_ADDRESS)); + + // Deposit + uint256 shares = token.deposit(depositAmount, user1, user1, minimumMint); + + console.log("Shares received:", shares); + // TODO: Add assertions + + //console.log("pUSD balance after deposit:", token.balanceOf(user1)); + //console.log("Asset balance in vault:", asset.balanceOf(address(vault))); + //assertEq(shares, depositAmount); + //assertEq(token.balanceOf(user1), depositAmount); + //assertEq(asset.balanceOf(address(vault)), depositAmount); + + vm.stopPrank(); + } + + function testRedeem() public skipIfNoRPC { + uint256 depositAmount = 1e6; + uint256 price = 1e6; // 1:1 price + uint256 minimumMint = depositAmount; + uint64 deadline = uint64(block.timestamp + 1 hours); + + // Setup + deal(address(asset), user1, depositAmount); + + vm.startPrank(user1); + + // Approve all necessary contracts + asset.approve(address(token), type(uint256).max); + asset.approve(address(vault), type(uint256).max); + asset.approve(TELLER_ADDRESS, type(uint256).max); + + // Additional approval needed for the vault to transfer from pUSD + vm.stopPrank(); + vm.startPrank(address(token)); + asset.approve(address(vault), type(uint256).max); + vm.stopPrank(); + + vm.startPrank(user1); + + // Perform deposit and redeem + token.deposit(depositAmount, user1, user1, minimumMint); + token.redeem(depositAmount, user1, user1, price, deadline); + + vm.stopPrank(); + + // TODO: warp time and verify final state + } + + function testTransfer() public skipIfNoRPC { + uint256 amount = 1e6; + + // Setup initial balance + deal(address(asset), user1, amount * 2); + + vm.startPrank(address(token)); + asset.approve(address(vault), type(uint256).max); + asset.approve(TELLER_ADDRESS, type(uint256).max); + vm.stopPrank(); + + vm.startPrank(user1); + + // First approve and deposit + asset.approve(address(token), type(uint256).max); + asset.approve(address(vault), type(uint256).max); + asset.approve(TELLER_ADDRESS, type(uint256).max); + + token.deposit(amount, user1, user1, amount); + + // Now test transfer + uint256 preBalance = token.balanceOf(user1); + token.transfer(user2, amount); + + assertEq(token.balanceOf(user1), preBalance - amount); + assertEq(token.balanceOf(user2), amount); + + vm.stopPrank(); + } + + function testVaultIntegration() public skipIfNoRPC { + uint256 amount = 1e6; + + vm.startPrank(user1); + token.deposit(amount, user1, user1); + //token.transfer(user2, amount); + vm.stopPrank(); + + //assertEq(vault.balanceOf(user1), 0); + //assertEq(vault.balanceOf(user2), amount); + //assertEq(asset.balanceOf(address(vault)), amount); + } + + function testVault() public skipIfNoRPC { + // Verify the vault address matches what we set in setUp + assertEq(address(token.getVault()), address(VAULT_ADDRESS)); + assertEq(address(token.getTeller()), address(TELLER_ADDRESS)); + assertEq(address(token.getAtomicQueue()), address(ATOMIC_QUEUE_ADDRESS)); + } + + function testSupportsInterface() public skipIfNoRPC { + // Test for ERC20 interface + bytes4 erc20InterfaceId = type(IERC20).interfaceId; + assertTrue(token.supportsInterface(erc20InterfaceId)); + + // Test for AccessControl interface + bytes4 accessControlInterfaceId = type(IAccessControl).interfaceId; + assertTrue(token.supportsInterface(accessControlInterfaceId)); + + // Test for non-supported interface + bytes4 randomInterfaceId = bytes4(keccak256("random()")); + assertFalse(token.supportsInterface(randomInterfaceId)); + } + // small hack to be excluded from coverage report + + function test() public { } + +} diff --git a/nest/test/vault.t.sol b/nest/test/vault.t.sol new file mode 100644 index 0000000..946f914 --- /dev/null +++ b/nest/test/vault.t.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { MockVault } from "../src/mocks/MockVault.sol"; +import { pUSD } from "../src/token/pUSD.sol"; + +import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol"; + +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +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 { Test } from "forge-std/Test.sol"; +import { console } from "forge-std/console.sol"; +import {Auth, Authority} from "@solmate/auth/Auth.sol"; + +contract TestUSDC is ERC20 { + + constructor() ERC20("USD Coin", "USDC") { + _mint(msg.sender, 1_000_000 * 10 ** 6); + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } + + function decimals() public pure override returns (uint8) { + return 6; + } + +} + +contract pUSDPlumeTest is Test { + + pUSD public token; + IERC20 public asset; + IERC4626 public vault; + + address public owner; + address public user1; + address public user2; + + // Constants for deployed contracts + address constant USDC_ADDRESS = 0x401eCb1D350407f13ba348573E5630B83638E30D; + address constant VAULT_ADDRESS = 0xe644F07B1316f28a7F134998e021eA9f7135F351; + address constant PUSD_PROXY = 0xF66DFD0A9304D3D6ba76Ac578c31C84Dc0bd4A00; + + event VaultChanged(IERC4626 indexed oldVault, IERC4626 indexed newVault); + + +function setUp() public { + // Fork Plume testnet + string memory PLUME_RPC = vm.envString("PLUME_RPC_URL"); + vm.createSelectFork(PLUME_RPC); + + // Setup accounts using the private key + uint256 privateKey = 0xf1906c3250e18e8036273019f2d6d4d5107404b84753068fe8fb170674461f1b; + owner = vm.addr(privateKey); + user1 = vm.addr(privateKey); + user2 = address(0x2); + + // Set the default signer for all transactions + vm.startPrank(owner, owner); + + // Connect to deployed contracts + token = pUSD(PUSD_PROXY); + asset = IERC20(USDC_ADDRESS); + vault = IERC4626(VAULT_ADDRESS); + + // No need to deal USDC if the account already has balance + // But we still need the approval + asset.approve(address(token), type(uint256).max); + + vm.stopPrank(); +} + + +function checkOwnership() public view { + Auth vaultAuth = Auth(address(VAULT_ADDRESS)); + Auth authorityContract = Auth(0xe88FAdd44F65a64ffB807c5C1aF2EADCAA2BBcCC); + + console.log("Vault owner:", vaultAuth.owner()); + console.log("Authority owner:", authorityContract.owner()); + console.log("Vault authority:", address(vaultAuth.authority())); +} + + + + + +}