diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..79e42de --- /dev/null +++ b/LICENSE @@ -0,0 +1,99 @@ +Business Source License 1.1 + +License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. +"Business Source License" is a trademark of MariaDB Corporation Ab. + +----------------------------------------------------------------------------- + +Parameters + +Licensor: Intuition Systems, Inc. + +Licensed Work: Intuition Protocol Contracts + The Licensed Work is (c) 2024 Intuition Systems, Inc. + +Additional Use Grant: None. + +Change Date: The earlier of 2026-06-01 (June 1st, 2026) or a date + specified at https:// + +Change License: MIT + +----------------------------------------------------------------------------- + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited +production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. + +MariaDB hereby grants you permission to use this License’s text to license +your works, and to refer to it using the trademark "Business Source License", +as long as you comply with the Covenants of Licensor below. + +----------------------------------------------------------------------------- + +Covenants of Licensor + +In consideration of the right to use this License’s text and the "Business +Source License" name and trademark, Licensor covenants to MariaDB, and to all +other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 2.0 or any later version, + or a license that is compatible with GPL Version 2.0 or a later version, + where "compatible" means that software provided under the Change License can + be included in a program with software provided under GPL Version 2.0 or a + later version. Licensor may specify additional Change Licenses without + limitation. + +2. To either: (a) specify an additional grant of rights to use that does not + impose any additional restriction on the right granted in this License, as + the Additional Use Grant; or (b) insert the text "None". + +3. To specify a Change Date. + +4. Not to modify this License in any other way. + +----------------------------------------------------------------------------- + +Notice + +The Business Source License (this document, or the "License") is not an Open +Source license. However, the Licensed Work will eventually be made available +under an Open Source License, as stated in this License. \ No newline at end of file diff --git a/README.md b/README.md index 14e4cb3..d8d128f 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# Intuition Protocol \ No newline at end of file +# Intuition Protocol + +The open source contracts of the Intuition protocol v1. \ No newline at end of file diff --git a/src/AtomWallet.sol b/src/AtomWallet.sol new file mode 100644 index 0000000..2e3e0ee --- /dev/null +++ b/src/AtomWallet.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.21; + +import {IEthMultiVault} from "src/interfaces/IEthMultiVault.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {BaseAccount, UserOperation} from "account-abstraction/contracts/core/BaseAccount.sol"; +import {IEntryPoint} from "account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import {Errors} from "./libraries/Errors.sol"; + +/** + * @title AtomWallet + * @notice This contract is an abstract account associated with a corresponding atom. + */ +contract AtomWallet is Initializable, BaseAccount, OwnableUpgradeable { + using ECDSA for bytes32; + + /// @notice The storage slot for the AtomWallet contract ownable storage + /// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Ownable")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant OwnableStorageLocation = 0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300; + + /// @notice The EthMultiVault contract address + IEthMultiVault public ethMultiVault; + + /// @notice The flag to indicate if the wallet's ownership has been claimed by the user + bool public isClaimed; + + /// @notice The entry point contract address + IEntryPoint private _entryPoint; + + // solhint-disable-next-line no-empty-blocks + receive() external payable {} + + /** + * @notice Initialize the AtomWallet contract + * @param anEntryPoint the entry point contract address + * @param anOwner the owner of the contract (`walletConfig.atomWarden` is the initial owner of all atom wallets) + * @param _ethMultiVault the EthMultiVault contract address + */ + function init(IEntryPoint anEntryPoint, address anOwner, IEthMultiVault _ethMultiVault) external initializer { + __Ownable_init(anOwner); + _entryPoint = anEntryPoint; + ethMultiVault = _ethMultiVault; + } + + /// @notice Get the entry point contract address + /// @return the entry point contract address + function entryPoint() public view virtual override returns (IEntryPoint) { + return _entryPoint; + } + + /** + * @notice Execute a transaction (called directly from owner, or by entryPoint) + * @param dest the target address + * @param value the value to send + * @param func the function call data + */ + function execute(address dest, uint256 value, bytes calldata func) external onlyOwnerOrEntryPoint { + _call(dest, value, func); + } + + /** + * @notice Execute a sequence (batch) of transactions + * @param dest the target addresses array + * @param func the function call data array + */ + function executeBatch(address[] calldata dest, bytes[] calldata func) external onlyOwnerOrEntryPoint { + if (dest.length != func.length) { + revert Errors.AtomWallet_WrongArrayLengths(); + } + + for (uint256 i = 0; i < dest.length; i++) { + _call(dest[i], 0, func[i]); + } + } + + /// implement template method of BaseAccount + /** + * @notice Validate the signature of the user operation + * @param userOp the user operation + * @param userOpHash the hash of the user operation + * @return validationData the validation data (0 if successful) + */ + function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash) + internal + virtual + override + returns (uint256 validationData) + { + bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", userOpHash)); + + (address recovered,,) = ECDSA.tryRecover(hash, userOp.signature); + + if (recovered != owner()) { + return SIG_VALIDATION_FAILED; + } + + return 0; + } + + /** + * @notice An internal method that calls a target address with value and data + * @param target the target address + * @param value the value to send + * @param data the function call data + */ + function _call(address target, uint256 value, bytes memory data) internal { + (bool success, bytes memory result) = target.call{value: value}(data); + if (!success) { + assembly { + revert(add(result, 32), mload(result)) + } + } + } + + /// @notice Returns the deposit of the account in the entry point contract + function getDeposit() public view returns (uint256) { + return entryPoint().balanceOf(address(this)); + } + + /// @notice Add deposit to the account in the entry point contract + function addDeposit() public payable { + entryPoint().depositTo{value: msg.value}(address(this)); + } + + /** + * @notice Withdraws value from the account's deposit + * @param withdrawAddress target to send to + * @param amount to withdraw + */ + function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public { + if (!(msg.sender == owner() || msg.sender == address(this))) { + revert Errors.AtomWallet_OnlyOwner(); + } + entryPoint().withdrawTo(withdrawAddress, amount); + } + + /// @notice Transfer ownership of the wallet to a new owner. Ifn the wallet's ownership + /// is being transferred to the user, the wallet is considered claimed. Once claimed, + /// wallet is considered owned by the user and the action cannot be undone. + /// @param newOwner the new owner of the wallet + function transferOwnership(address newOwner) public override onlyOwner { + if (newOwner == address(0)) { + revert OwnableInvalidOwner(address(0)); + } + + if (!isClaimed && newOwner != ethMultiVault.getAtomWarden()) { + isClaimed = true; + } + + _transferOwnership(newOwner); + } + + /// @notice Returns the owner of the wallet. If the wallet has been claimed, the owner + /// is the user. Otherwise, the owner is the atomWarden. + function owner() public view override returns (address) { + OwnableStorage storage $ = _getAtomWalletOwnableStorage(); + return isClaimed ? $._owner : ethMultiVault.getAtomWarden(); + } + + /// @dev Get the storage slot for the AtomWallet contract ownable storage + function _getAtomWalletOwnableStorage() private pure returns (OwnableStorage storage $) { + assembly { + $.slot := OwnableStorageLocation + } + } + + /// @dev Modifier to allow only the owner or entry point to call a function + modifier onlyOwnerOrEntryPoint() { + if (!(msg.sender == address(entryPoint()) || msg.sender == owner())) { + revert Errors.AtomWallet_OnlyOwnerOrEntryPoint(); + } + _; + } +} \ No newline at end of file diff --git a/src/EthMultiVault.sol b/src/EthMultiVault.sol new file mode 100644 index 0000000..ade5e79 --- /dev/null +++ b/src/EthMultiVault.sol @@ -0,0 +1,1351 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.21; + +import {IEthMultiVault} from "src/interfaces/IEthMultiVault.sol"; +import {AtomWallet} from "src/AtomWallet.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; +import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; +import {LibZip} from "solady/utils/LibZip.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; +import {SafeCastLib} from "solmate/utils/SafeCastLib.sol"; +import {IPermit2} from "src/interfaces/IPermit2.sol"; +import {IEntryPoint} from "account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import {Errors} from "src/libraries/Errors.sol"; + +/** + * @title EthMultiVault + * @notice Manages the creation and management of vaults associated with atoms & triples. + */ +contract EthMultiVault is IEthMultiVault, Initializable, ReentrancyGuardUpgradeable, PausableUpgradeable { + using FixedPointMathLib for uint256; + using LibZip for bytes; + + /* =================================================== */ + /* STATE VARIABLES */ + /* =================================================== */ + + /// @notice Configuration structs + GeneralConfig public generalConfig; + AtomConfig public atomConfig; + TripleConfig public tripleConfig; + WalletConfig public walletConfig; + + /// @notice ID of the last vault to be created + uint256 public count; + + // Operation identifiers + bytes32 public constant SET_ADMIN = keccak256("setAdmin"); + bytes32 public constant SET_EXIT_FEE = keccak256("setExitFee"); + + /// @notice Mapping of vault ID to vault state + // Vault ID -> Vault State + mapping(uint256 => VaultState) public vaults; + + /// @notice Mapping of vault ID to vault fees + // Vault ID -> Vault Fees + mapping(uint256 => VaultFees) public vaultFees; + + /// @notice RDF (Resource Description Framework) + // mapping of vault ID to atom data + // Vault ID -> Atom Data + mapping(uint256 => bytes) public atoms; + + // mapping of atom hash to atom vault ID + // Hash -> Atom ID + mapping(bytes32 => uint256) public atomsByHash; + + // mapping of triple vault ID to the underlying atom IDs that make up the triple + // Triple ID -> VaultIDs of atoms that make up the triple + mapping(uint256 => uint256[3]) public triples; + + // mapping of triple hash to triple vault ID + // Hash -> Triple ID + mapping(bytes32 => uint256) public triplesByHash; + + // mapping of triple vault IDs to determine whether a vault is a triple or not + // Vault ID -> (Is Triple) + mapping(uint256 => bool) public isTriple; + + /// @notice Atom Shares Tracking + /// used to enable atom shares earned from triple deposits to be redeemed proportionally + /// to the triple shares that earned them upon redemption/withdraw + /// Triple ID -> Atom ID -> Account Address -> Atom Share Balance + mapping(uint256 => mapping(uint256 => mapping(address => uint256))) public tripleAtomShares; + + /// @notice Timelock mapping (operation hash -> timelock struct) + mapping(bytes32 => Timelock) public timelocks; + + /* =================================================== */ + /* INITIALIZER */ + /* =================================================== */ + + /// @notice Initializes the MultiVault contract + /// @param _generalConfig General configuration struct + /// @param _atomConfig Atom configuration struct + /// @param _tripleConfig Triple configuration struct + /// @param _walletConfig Wallet configuration struct + /// @param _defaultVaultFees Default vault fees struct + /// @dev This function is called only once (during contract deployment) + function init( + GeneralConfig memory _generalConfig, + AtomConfig memory _atomConfig, + TripleConfig memory _tripleConfig, + WalletConfig memory _walletConfig, + VaultFees memory _defaultVaultFees + ) external initializer { + __ReentrancyGuard_init(); + __Pausable_init(); + + generalConfig = _generalConfig; + atomConfig = _atomConfig; + tripleConfig = _tripleConfig; + walletConfig = _walletConfig; + + vaultFees[0] = VaultFees({ + entryFee: _defaultVaultFees.entryFee, + exitFee: _defaultVaultFees.exitFee, + protocolFee: _defaultVaultFees.protocolFee + }); + } + + /* =================================================== */ + /* VIEWS */ + /* =================================================== */ + + /* -------------------------- */ + /* Fee Helpers */ + /* -------------------------- */ + + /// @notice returns the cost of creating an atom + /// @return atomCost the cost of creating an atom + function getAtomCost() public view returns (uint256) { + uint256 atomCost = atomConfig.atomCreationProtocolFee // paid to protocol + + atomConfig.atomWalletInitialDepositAmount // for purchasing shares for atom wallet + + generalConfig.minShare; // for purchasing ghost shares + return atomCost; + } + + /// @notice returns the cost of creating a triple + /// @return tripleCost the cost of creating a triple + function getTripleCost() public view returns (uint256) { + uint256 tripleCost = tripleConfig.tripleCreationProtocolFee // paid to protocol + + tripleConfig.atomDepositFractionOnTripleCreation // goes towards increasing the amount of assets in the underlying atom vaults + + generalConfig.minShare * 2; // for purchasing ghost shares for the positive and counter triple vaults + return tripleCost; + } + + /// @notice returns the total fees that would be charged for depositing 'assets' into a vault + /// @param assets amount of `assets` to calculate fees on + /// @param id vault id to get corresponding fees for + /// @return totalFees total fees that would be charged for depositing 'assets' into a vault + function getDepositFees(uint256 assets, uint256 id) public view returns (uint256) { + uint256 protocolFee = protocolFeeAmount(assets, id); + uint256 userAssetsAfterProtocolFees = assets - protocolFee; + + uint256 atomDepositFraction = atomDepositFractionAmount(userAssetsAfterProtocolFees, id); + uint256 userAssetsAfterProtocolFeesAndAtomDepositFraction = userAssetsAfterProtocolFees - atomDepositFraction; + + uint256 entryFee = entryFeeAmount(userAssetsAfterProtocolFeesAndAtomDepositFraction, id); + uint256 totalFees = protocolFee + atomDepositFraction + entryFee; + + return totalFees; + } + + /// @notice returns the shares for recipient and other important values when depositing 'assets' into a vault + /// @param assets amount of `assets` to calculate fees on (should always be msg.value - protocolFees) + /// @param id vault id to get corresponding fees for + /// @return totalAssetsDelta changes in vault's total assets + /// @return sharesForReceiver changes in vault's total shares (shares owed to receiver) + /// @return userAssetsAfterTotalFees amount of assets that goes towards minting shares for the receiver + /// @return entryFee amount of assets that would be charged for the entry fee + function getDepositSharesAndFees(uint256 assets, uint256 id) + public + view + returns (uint256, uint256, uint256, uint256) + { + uint256 atomDepositFraction = atomDepositFractionAmount(assets, id); + uint256 userAssetsAfterAtomDepositFraction = assets - atomDepositFraction; + + // changes in vault's total assets + // if the vault is an atom vault `atomDepositFraction` is 0 + uint256 totalAssetsDelta = assets - atomDepositFraction; + + uint256 entryFee; + + if (vaults[id].totalShares == generalConfig.minShare) { + entryFee = 0; + } else { + entryFee = entryFeeAmount(userAssetsAfterAtomDepositFraction, id); + } + + // amount of assets that goes towards minting shares for the receiver + uint256 userAssetsAfterTotalFees = userAssetsAfterAtomDepositFraction - entryFee; + + // user receives amount of shares as calculated by `convertToShares` + uint256 sharesForReceiver = convertToShares(userAssetsAfterTotalFees, id); + + return (totalAssetsDelta, sharesForReceiver, userAssetsAfterTotalFees, entryFee); + } + + /// @notice returns the assets for receiver and other important values when redeeming 'shares' from a vault + /// @param shares amount of `shares` to calculate fees on + /// @param id vault id to get corresponding fees for + /// @return totalUserAssets total amount of assets user would receive if redeeming 'shares', not including fees + /// @return assetsForReceiver amount of assets that is redeemable by the receiver + /// @return protocolFees amount of assets that would be sent to the protocol vault + /// @return exitFees amount of assets that would be charged for the exit fee + function getRedeemAssetsAndFees(uint256 shares, uint256 id) + public + view + returns (uint256, uint256, uint256, uint256) + { + uint256 remainingShares = vaults[id].totalShares - shares; + + uint256 assetsForReceiverBeforeFees = convertToAssets(shares, id); + uint256 protocolFees; + uint256 exitFees; + + /* + * if the redeem amount results in a zero share balance for + * the associated vault, no exit fee is charged to avoid + * zero address accumulating disproportionate fee revenue via ghost + * shares. Also, in case of an emergency redemption (i.e. when the + * contract is paused), no exit fees are charged either. + */ + if (paused()) { + exitFees = 0; + protocolFees = 0; + } else if (remainingShares == generalConfig.minShare) { + exitFees = 0; + protocolFees = protocolFeeAmount(assetsForReceiverBeforeFees, id); + } else { + protocolFees = protocolFeeAmount(assetsForReceiverBeforeFees, id); + uint256 assetsForReceiverAfterProtocolFees = assetsForReceiverBeforeFees - protocolFees; + exitFees = exitFeeAmount(assetsForReceiverAfterProtocolFees, id); + } + + uint256 totalUserAssets = assetsForReceiverBeforeFees; + uint256 assetsForReceiver = assetsForReceiverBeforeFees - exitFees - protocolFees; + + return (totalUserAssets, assetsForReceiver, protocolFees, exitFees); + } + + /// @notice calculates fee on raw amount + /// @param amount amount of assets to calculate fee on + /// @param fee fee in % + /// @return amount of assets that would be charged as fee + function _feeOnRaw(uint256 amount, uint256 fee) internal view returns (uint256) { + return amount.mulDivUp(fee, generalConfig.feeDenominator); + } + + /// @notice returns amount of assets that would be charged for the entry fee given an amount of 'assets' provided + /// @param assets amount of assets to calculate fee on + /// @param id vault id to get corresponding fees for + /// @return feeAmount amount of assets that would be charged for the entry fee + /// NOTE: if the vault being deposited on has a vault total shares of 0, the entry fee is not applied + function entryFeeAmount(uint256 assets, uint256 id) public view returns (uint256) { + uint256 entryFees = vaultFees[id].entryFee; + uint256 feeAmount = _feeOnRaw(assets, entryFees == 0 ? vaultFees[0].entryFee : entryFees); + return feeAmount; + } + + /// @notice returns amount of assets that would be charged for the exit fee given an amount of 'assets' provided + /// @param assets amount of assets to calculate fee on + /// @param id vault id to get corresponding fees for + /// @return feeAmount amount of assets that would be charged for the exit fee + /// NOTE: if the vault being redeemed from given the shares to redeem results in a total shares after of 0, + /// the exit fee is not applied + function exitFeeAmount(uint256 assets, uint256 id) public view returns (uint256) { + uint256 exitFees = vaultFees[id].exitFee; + uint256 feeAmount = _feeOnRaw(assets, exitFees == 0 ? vaultFees[0].exitFee : exitFees); + return feeAmount; + } + + /// @notice returns amount of assets that would be charged by a vault on protocol fee given amount of 'assets' + /// provided + /// @param assets amount of assets to calculate fee on + /// @param id vault id to get corresponding fees for + /// @return feeAmount amount of assets that would be charged by vault on protocol fee + function protocolFeeAmount(uint256 assets, uint256 id) public view returns (uint256) { + uint256 protocolFees = vaultFees[id].protocolFee; + uint256 feeAmount = _feeOnRaw(assets, protocolFees == 0 ? vaultFees[0].protocolFee : protocolFees); + return feeAmount; + } + + /// @notice returns atom deposit fraction given amount of 'assets' provided + /// @param assets amount of assets to calculate fee on + /// @param id vault id + /// @return feeAmount amount of assets that would be used as atom deposit fraction + /// NOTE: only applies to triple vaults + function atomDepositFractionAmount(uint256 assets, uint256 id) public view returns (uint256) { + uint256 feeAmount = isTripleId(id) ? _feeOnRaw(assets, tripleConfig.atomDepositFractionForTriple) : 0; + return feeAmount; + } + + /* -------------------------- */ + /* Accounting Helpers */ + /* -------------------------- */ + + /// @notice returns amount of shares that would be exchanged by vault given amount of 'assets' provided + /// @param assets amount of assets to calculate shares on + /// @param id vault id to get corresponding shares for + /// @return shares amount of shares that would be exchanged by vault given amount of 'assets' provided + function convertToShares(uint256 assets, uint256 id) public view returns (uint256) { + uint256 supply = vaults[id].totalShares; + uint256 shares = supply == 0 ? assets : assets.mulDiv(supply, vaults[id].totalAssets); + return shares; + } + + /// @notice returns amount of assets that would be exchanged by vault given amount of 'shares' provided + /// @param shares amount of shares to calculate assets on + /// @param id vault id to get corresponding assets for + /// @return assets amount of assets that would be exchanged by vault given amount of 'shares' provided + function convertToAssets(uint256 shares, uint256 id) public view returns (uint256) { + uint256 supply = vaults[id].totalShares; + uint256 assets = supply == 0 ? shares : shares.mulDiv(vaults[id].totalAssets, supply); + return assets; + } + + /// @notice returns the current share price for the given vault id + /// @param id vault id to get corresponding share price for + /// @return price current share price for the given vault id + function currentSharePrice(uint256 id) external view returns (uint256) { + uint256 supply = vaults[id].totalShares; + uint256 price = supply == 0 ? 0 : (vaults[id].totalAssets * generalConfig.decimalPrecision) / supply; + return price; + } + + /// @notice simulates the effects of the deposited amount of 'assets' and returns the estimated + /// amount of shares that would be minted from the deposit of `assets` + /// @param assets amount of assets to calculate shares on + /// @param id vault id to get corresponding shares for + /// @return shares amount of shares that would be minted from the deposit of `assets` + /// NOTE: this function pessimistically estimates the amount of shares that would be minted from the + /// input amount of assets so if the vault is empty before the deposit the caller receives more + /// shares than returned by this function, reference internal _depositIntoVault logic for details + function previewDeposit( + uint256 assets, // should always be msg.value + uint256 id + ) public view returns (uint256) { + (, uint256 sharesForReceiver,,) = getDepositSharesAndFees(assets, id); + return sharesForReceiver; + } + + /// @notice simulates the effects of the redemption of `shares` and returns the estimated + /// amount of assets estimated to be returned to the receiver of the redeem + /// @param shares amount of shares to calculate assets on + /// @param id vault id to get corresponding assets for + /// @return assets amount of assets estimated to be returned to the receiver + function previewRedeem(uint256 shares, uint256 id) public view returns (uint256) { + (, uint256 assetsForReceiver,,) = getRedeemAssetsAndFees(shares, id); + return assetsForReceiver; + } + + /// @notice returns max amount of shares that can be redeemed from the 'owner' balance through a redeem call + /// @param owner address of the account to get max redeemable shares for + /// @param id vault id to get corresponding shares for + /// @return shares amount of shares that can be redeemed from the 'owner' balance through a redeem call + function maxRedeem(address owner, uint256 id) external view returns (uint256) { + uint256 shares = vaults[id].balanceOf[owner]; + return shares; + } + + /* -------------------------- */ + /* Triple Helpers */ + /* -------------------------- */ + + /// @notice returns the corresponding hash for the given RDF triple, given the atoms that make up the triple + /// @param subjectId the subject atom's vault id + /// @param predicateId the predicate atom's vault id + /// @param objectId the object atom's vault id + /// @return hash the corresponding hash for the given RDF triple based on the atom vault ids + function tripleHashFromAtoms(uint256 subjectId, uint256 predicateId, uint256 objectId) + public + pure + returns (bytes32) + { + return keccak256(abi.encodePacked(subjectId, predicateId, objectId)); + } + + /// @notice returns the corresponding hash for the given RDF triple, given the triple vault id + /// @param id vault id of the triple + /// @return hash the corresponding hash for the given RDF triple + /// NOTE: only applies to triple vault IDs as input + function tripleHash(uint256 id) public view returns (bytes32) { + uint256[3] memory atomIds; + (atomIds[0], atomIds[1], atomIds[2]) = getTripleAtoms(id); + return keccak256(abi.encodePacked(atomIds[0], atomIds[1], atomIds[2])); + } + + /// @notice returns whether the supplied vault id is a triple + /// @param id vault id to check + /// @return bool whether the supplied vault id is a triple + function isTripleId(uint256 id) public view returns (bool) { + bool isCounterTriple = id > type(uint256).max / 2; + return isCounterTriple ? isTriple[type(uint256).max - id] : isTriple[id]; + } + + /// @notice returns the atoms that make up a triple/counter-triple + /// @param id vault id of the triple/counter-triple + /// @return tuple(atomIds) the atoms that make up the triple/counter-triple + /// NOTE: only applies to triple vault IDs as input + function getTripleAtoms(uint256 id) public view returns (uint256, uint256, uint256) { + bool isCounterTriple = id > type(uint256).max / 2; + uint256[3] memory atomIds = isCounterTriple ? triples[type(uint256).max - id] : triples[id]; + return (atomIds[0], atomIds[1], atomIds[2]); + } + + /// @notice returns the counter id from the given triple id + /// @param id vault id of the triple + /// @return counterId the counter vault id from the given triple id + /// NOTE: only applies to triple vault IDs as input + function getCounterIdFromTriple(uint256 id) public pure returns (uint256) { + return type(uint256).max - id; + } + + /* -------------------------- */ + /* Misc. Helpers */ + /* -------------------------- */ + + /// @notice returns the number of shares and assets (less fees) user has in the vault + /// @param vaultId vault id of the vault + /// @param receiver address of the receiver + /// @return shares number of shares user has in the vault + /// @return assets number of assets user has in the vault + function getVaultStateForUser(uint256 vaultId, address receiver) external view returns (uint256, uint256) { + uint256 shares = vaults[vaultId].balanceOf[receiver]; + (uint256 totalUserAssets,,,) = getRedeemAssetsAndFees(shares, vaultId); + return (shares, totalUserAssets); + } + + /// @dev checks if an account holds shares in the vault counter to the id provided + /// @param id the id of the vault to check + /// @param receiver the account to check + /// @return bool whether the account holds shares in the counter vault to the id provided or not + function _hasCounterStake(uint256 id, address receiver) internal view returns (bool) { + if (!isTripleId(id)) { + revert Errors.MultiVault_VaultNotTriple(); + } + + return vaults[type(uint256).max - id].balanceOf[receiver] > 0; + } + + /// @dev getDeploymentData - returns the deployment data for the AtomWallet contract + /// @return bytes memory the deployment data for the AtomWallet contract (using BeaconProxy pattern) + function _getDeploymentData() internal view returns (bytes memory) { + // Address of the atomWalletBeacon contract + address beaconAddress = walletConfig.atomWalletBeacon; + + // BeaconProxy creation code + bytes memory code = type(BeaconProxy).creationCode; + + // encode the init function of the AtomWallet contract with the entryPoint and atomWarden as constructor arguments + bytes memory initData = abi.encodeWithSelector( + AtomWallet.init.selector, IEntryPoint(walletConfig.entryPoint), walletConfig.atomWarden, address(this) + ); + + // encode constructor arguments of the BeaconProxy contract (address beacon, bytes memory data) + bytes memory encodedArgs = abi.encode(beaconAddress, initData); + + // concatenate the BeaconProxy creation code with the ABI-encoded constructor arguments + return abi.encodePacked(code, encodedArgs); + } + + /// @notice returns the Atom Wallet address for the given atom data + /// @param id vault id of the atom associated to the atom wallet + /// @return atomWallet the address of the atom wallet + /// NOTE: the create2 salt is based off of the vault ID + function computeAtomWalletAddr(uint256 id) public view returns (address) { + // compute salt for create2 + bytes32 salt = bytes32(id); + + // get contract deployment data + bytes memory data = _getDeploymentData(); + + // compute the raw contract address + bytes32 rawAddress = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(data))); + + return address(bytes20(rawAddress << 96)); + } + + /// @notice returns the address of the atom warden + function getAtomWarden() external view returns (address) { + return walletConfig.atomWarden; + } + + /* =================================================== */ + /* MUTATIVE FUNCTIONS */ + /* =================================================== */ + + /* -------------------------- */ + /* Atom Wallet */ + /* -------------------------- */ + + /// @notice deploy a given atom wallet + /// @param atomId vault id of atom + /// @return atomWallet the address of the atom wallet + /// NOTE: deploys an ERC4337 account (atom wallet) through a BeaconProxy. Reverts if the atom vault does not exist + function deployAtomWallet(uint256 atomId) external whenNotPaused returns (address) { + if (atomId == 0 || atomId > count) { + revert Errors.MultiVault_VaultDoesNotExist(); + } + + // compute salt for create2 + bytes32 salt = bytes32(atomId); + + // get contract deployment data + bytes memory data = _getDeploymentData(); + + address atomWallet; + + // deploy atom wallet with create2: + // value sent in wei, + // memory offset of `code` (after first 32 bytes where the length is), + // length of `code` (first 32 bytes of code), + // salt for create2 + assembly { + atomWallet := create2(0, add(data, 0x20), mload(data), salt) + } + + if (atomWallet == address(0)) { + revert Errors.MultiVault_DeployAccountFailed(); + } + + return atomWallet; + } + + /* -------------------------- */ + /* Create Atom */ + /* -------------------------- */ + + /// @notice Create an atom and return its vault id + /// @param atomUri atom data to create atom with + /// @return id vault id of the atom + /// NOTE: This function will revert if called with less than `getAtomCost()` in `msg.value` + function createAtom(bytes calldata atomUri) external payable nonReentrant whenNotPaused returns (uint256) { + if (msg.value < getAtomCost()) { + revert Errors.MultiVault_InsufficientBalance(); + } + + // create atom and get the protocol deposit fee + (uint256 id, uint256 protocolDepositFee) = _createAtom(atomUri, msg.value); + + uint256 totalFeesForProtocol = atomConfig.atomCreationProtocolFee + protocolDepositFee; + _transferFeesToProtocolVault(totalFeesForProtocol); + + return id; + } + + /// @notice Batch create atoms and return their vault ids + /// @param atomUris atom data array to create atoms with + /// @return ids vault ids array of the atoms + /// NOTE: This function will revert if called with less than `getAtomCost()` * `atomUris.length` in `msg.value` + function batchCreateAtom(bytes[] calldata atomUris) + external + payable + nonReentrant + whenNotPaused + returns (uint256[] memory) + { + uint256 length = atomUris.length; + if (msg.value < getAtomCost() * length) { + revert Errors.MultiVault_InsufficientBalance(); + } + + uint256 valuePerAtom = msg.value / length; + uint256 protocolDepositFeeTotal; + uint256[] memory ids = new uint256[](length); + + for (uint256 i = 0; i < length; i++) { + uint256 protocolDepositFee; + (ids[i], protocolDepositFee) = _createAtom(atomUris[i], valuePerAtom); + + // add protocol deposit fees to total + protocolDepositFeeTotal += protocolDepositFee; + } + + uint256 totalFeesForProtocol = atomConfig.atomCreationProtocolFee * length + protocolDepositFeeTotal; + _transferFeesToProtocolVault(totalFeesForProtocol); + + return ids; + } + + /// @notice Internal utility function to create an atom and handle vault creation + /// @param atomUri The atom data to create an atom with + /// @param value The value sent with the transaction + /// @return id The new vault ID created for the atom + function _createAtom(bytes calldata atomUri, uint256 value) internal returns (uint256, uint256) { + if (atomUri.length > generalConfig.atomUriMaxLength) { + revert Errors.MultiVault_AtomUriTooLong(); + } + + uint256 atomCost = getAtomCost(); + + // check if atom already exists based on hash + bytes32 hash = keccak256(atomUri); + if (atomsByHash[hash] != 0) { + revert Errors.MultiVault_AtomExists(atomUri); + } + + // calculate user deposit amount + uint256 userDeposit = value - atomCost; + + // create a new atom vault + uint256 id = _createVault(); + + // calculate protocol deposit fee + uint256 protocolDepositFee = protocolFeeAmount(userDeposit, id); + + // calculate user deposit after protocol fees + uint256 userDepositAfterProtocolFees = userDeposit - protocolDepositFee; + + // deposit user funds into vault and mint shares for the user and shares for the zero address + _depositOnVaultCreation( + id, + msg.sender, // receiver + userDepositAfterProtocolFees + ); + + // get atom wallet address for the corresponding atom + address atomWallet = computeAtomWalletAddr(id); + + // deposit atomWalletInitialDepositAmount amount of assets and mint the shares for the atom wallet + _depositOnVaultCreation( + id, + atomWallet, // receiver + atomConfig.atomWalletInitialDepositAmount + ); + + // map the new vault ID to the atom data + atoms[id] = atomUri; + + // map the resultant atom hash to the new vault ID + atomsByHash[hash] = id; + + emit AtomCreated(msg.sender, atomWallet, atomUri, id); + + return (id, protocolDepositFee); + } + + /* -------------------------- */ + /* Create Triple */ + /* -------------------------- */ + + /// @notice create a triple and return its vault id + /// @param subjectId vault id of the subject atom + /// @param predicateId vault id of the predicate atom + /// @param objectId vault id of the object atom + /// @return id vault id of the triple + /// NOTE: This function will revert if called with less than `getTripleCost()` in `msg.value`. + /// This function will revert if any of the atoms do not exist or if any ids are triple vaults. + function createTriple(uint256 subjectId, uint256 predicateId, uint256 objectId) + external + payable + nonReentrant + whenNotPaused + returns (uint256) + { + if (msg.value < getTripleCost()) { + revert Errors.MultiVault_InsufficientBalance(); + } + + // create triple and get the protocol deposit fee + (uint256 id, uint256 protocolDepositFee) = _createTriple(subjectId, predicateId, objectId, msg.value); + + uint256 totalFeesForProtocol = tripleConfig.tripleCreationProtocolFee + protocolDepositFee; + _transferFeesToProtocolVault(totalFeesForProtocol); + + return id; + } + + /// @notice batch create triples and return their vault ids + /// @param subjectIds vault ids array of subject atoms + /// @param predicateIds vault ids array of predicate atoms + /// @param objectIds vault ids array of object atoms + /// NOTE: This function will revert if called with less than `getTripleCost()` * `array.length` in `msg.value`. + /// This function will revert if any of the atoms do not exist or if any ids are triple vaults. + function batchCreateTriple( + uint256[] calldata subjectIds, + uint256[] calldata predicateIds, + uint256[] calldata objectIds + ) external payable nonReentrant whenNotPaused returns (uint256[] memory) { + if (subjectIds.length != predicateIds.length || subjectIds.length != objectIds.length) { + revert Errors.MultiVault_ArraysNotSameLength(); + } + + uint256 length = subjectIds.length; + uint256 tripleCost = getTripleCost(); + if (msg.value < tripleCost * length) { + revert Errors.MultiVault_InsufficientBalance(); + } + + uint256 valuePerTriple = msg.value / length; + uint256 protocolDepositFeeTotal; + uint256[] memory ids = new uint256[](length); + + for (uint256 i = 0; i < length; i++) { + uint256 protocolDepositFee; + (ids[i], protocolDepositFee) = _createTriple(subjectIds[i], predicateIds[i], objectIds[i], valuePerTriple); + + // add protocol deposit fees to total + protocolDepositFeeTotal += protocolDepositFee; + } + + uint256 totalFeesForProtocol = tripleConfig.tripleCreationProtocolFee * length + protocolDepositFeeTotal; + _transferFeesToProtocolVault(totalFeesForProtocol); + + return ids; + } + + /// @notice Internal utility function to create a triple + /// @param subjectId vault id of the subject atom + /// @param predicateId vault id of the predicate atom + /// @param objectId vault id of the object atom + /// @param value The amount of ETH the user has sent minus the base triple cost + /// @return id The new vault ID of the created triple + /// @return protocolDepositFee The calculated protocol fee for the deposit + function _createTriple(uint256 subjectId, uint256 predicateId, uint256 objectId, uint256 value) + internal + returns (uint256, uint256) + { + uint256[3] memory tripleAtomIds = [subjectId, predicateId, objectId]; + + for (uint256 i = 0; i < 3; i++) { + // make sure atoms exist, if not, revert + if (tripleAtomIds[i] == 0 || tripleAtomIds[i] > count) { + revert Errors.MultiVault_AtomDoesNotExist(tripleAtomIds[i]); + } + + // make sure that each id is not a triple vault id + if (isTripleId(tripleAtomIds[i])) { + revert Errors.MultiVault_VaultIsTriple(tripleAtomIds[i]); + } + } + + // check if triple already exists + bytes32 hash = tripleHashFromAtoms(subjectId, predicateId, objectId); + + if (triplesByHash[hash] != 0) { + revert Errors.MultiVault_TripleExists(subjectId, predicateId, objectId); + } + + // calculate user deposit amount + uint256 userDeposit = value - getTripleCost(); + + // create a new positive triple vault + uint256 id = _createVault(); + + // calculate protocol deposit fee + uint256 protocolDepositFee = protocolFeeAmount(userDeposit, id); + + // calculate user deposit after protocol fees + uint256 userDepositAfterProtocolFees = userDeposit - protocolDepositFee; + + // map the resultant triple hash to the new vault ID of the triple + triplesByHash[hash] = id; + + // map the triple's vault ID to the underlying atom vault IDs + triples[id] = [subjectId, predicateId, objectId]; + + // set this new triple's vault ID as true in the IsTriple mapping as well as its counter + isTriple[id] = true; + + uint256 atomDepositFraction = atomDepositFractionAmount(userDepositAfterProtocolFees, id); + + // give the user shares in the positive triple vault + _depositOnVaultCreation( + id, + msg.sender, // receiver + userDepositAfterProtocolFees - atomDepositFraction + ); + + // deposit assets into each underlying atom vault and mint shares for the receiver + if (atomDepositFraction > 0) { + _depositAtomFraction( + id, + msg.sender, // receiver + atomDepositFraction + ); + } + + for (uint256 i = 0; i < 3; i++) { + uint256 atomId = tripleAtomIds[i]; + // increase the total assets in each underlying atom vault + _setVaultTotals( + atomId, + vaults[atomId].totalAssets + (tripleConfig.atomDepositFractionOnTripleCreation / 3), + vaults[atomId].totalShares + ); + } + + emit TripleCreated(msg.sender, subjectId, predicateId, objectId, id); + + return (id, protocolDepositFee); + } + + /* -------------------------- */ + /* Deposit/Redeem Atom */ + /* -------------------------- */ + + /// @notice deposit eth into an atom vault and grant ownership of 'shares' to 'reciever' + /// *payable msg.value amount of eth to deposit + /// @dev assets parameter is omitted in favor of msg.value, unlike in ERC4626 + /// @param receiver the address to receive the shares + /// @param id the vault ID of the atom + /// @return shares the amount of shares minted + /// NOTE: this function will revert if the minimum deposit amount of eth is not met and + /// if the vault ID does not exist/is not an atom. + function depositAtom(address receiver, uint256 id) external payable nonReentrant whenNotPaused returns (uint256) { + if (id == 0 || id > count) { + revert Errors.MultiVault_VaultDoesNotExist(); + } + + if (isTripleId(id)) { + revert Errors.MultiVault_VaultNotAtom(); + } + + if (msg.value < generalConfig.minDeposit) { + revert Errors.MultiVault_MinimumDeposit(); + } + + uint256 protocolFees = protocolFeeAmount(msg.value, id); + uint256 userDepositAfterProtocolFees = msg.value - protocolFees; + + // deposit eth into vault and mint shares for the receiver + uint256 shares = _deposit(receiver, id, userDepositAfterProtocolFees); + + _transferFeesToProtocolVault(protocolFees); + + return shares; + } + + /// @notice redeem assets from an atom vault + /// @param shares the amount of shares to redeem + /// @param receiver the address to receiver the assets + /// @param id the vault ID of the atom + /// @return assets the amount of assets/eth withdrawn + /// NOTE: Emergency redemptions without any fees being charged are always possible, even if the contract is paused + /// See `getRedeemAssetsAndFees` for more details on the fees charged + function redeemAtom(uint256 shares, address receiver, uint256 id) external nonReentrant returns (uint256) { + if (id == 0 || id > count) { + revert Errors.MultiVault_VaultDoesNotExist(); + } + + if (isTripleId(id)) { + revert Errors.MultiVault_VaultNotAtom(); + } + + /* + withdraw shares from vault, returning the amount of + assets to be transferred to the receiver + */ + (uint256 assets, uint256 protocolFees) = _redeem(id, msg.sender, shares); + + // transfer eth to receiver factoring in fees/shares + (bool success,) = payable(receiver).call{value: assets}(""); + if (!success) { + revert Errors.MultiVault_TransferFailed(); + } + + _transferFeesToProtocolVault(protocolFees); + + return assets; + } + + /* -------------------------- */ + /* Deposit/Redeem Triple */ + /* -------------------------- */ + + /// @notice deposits assets of underlying tokens into a triple vault and grants ownership of 'shares' to 'receiver' + /// *payable msg.value amount of eth to deposit + /// @dev assets parameter is omitted in favor of msg.value, unlike in ERC4626 + /// @param receiver the address to receive the shares + /// @param id the vault ID of the triple + /// @return shares the amount of shares minted + /// NOTE: this function will revert if the minimum deposit amount of eth is not met and + /// if the vault ID does not exist/is not a triple. + function depositTriple(address receiver, uint256 id) + external + payable + nonReentrant + whenNotPaused + returns (uint256) + { + if (!isTripleId(id)) { + revert Errors.MultiVault_VaultNotTriple(); + } + + if (_hasCounterStake(id, receiver)) { + revert Errors.MultiVault_HasCounterStake(); + } + + if (msg.value < generalConfig.minDeposit) { + revert Errors.MultiVault_MinimumDeposit(); + } + + uint256 protocolFees = protocolFeeAmount(msg.value, id); + uint256 userDepositAfterProtocolFees = msg.value - protocolFees; + + // deposit eth into vault and mint shares for the receiver + uint256 shares = _deposit(receiver, id, userDepositAfterProtocolFees); + + _transferFeesToProtocolVault(protocolFees); + + // distribute atom shares for all 3 atoms that underly the triple + uint256 atomDepositFraction = atomDepositFractionAmount(userDepositAfterProtocolFees, id); + _depositAtomFraction(id, receiver, atomDepositFraction); + + return shares; + } + + /// @notice redeems 'shares' number of shares from the triple vault and send 'assets' eth + /// from the multiVault to 'reciever' factoring in exit fees + /// @param shares the amount of shares to redeem + /// @param receiver the address to receiver the assets + /// @param id the vault ID of the triple + /// @return assets the amount of assets/eth withdrawn + /// NOTE: Emergency redemptions without any fees being charged are always possible, even if the contract is paused + /// See `getRedeemAssetsAndFees` for more details on the fees charged + function redeemTriple(uint256 shares, address receiver, uint256 id) external nonReentrant returns (uint256) { + if (!isTripleId(id)) { + revert Errors.MultiVault_VaultNotTriple(); + } + + /* + withdraw shares from vault, returning the amount of + assets to be transferred to the receiver + */ + (uint256 assets, uint256 protocolFees) = _redeem(id, msg.sender, shares); + + // transfer eth to receiver factoring in fees/shares + (bool success,) = payable(receiver).call{value: assets}(""); + if (!success) { + revert Errors.MultiVault_TransferFailed(); + } + + _transferFeesToProtocolVault(protocolFees); + + return assets; + } + + /* =================================================== */ + /* INTERNAL METHODS */ + /* =================================================== */ + + /// @dev transfer fees to the protocol vault + function _transferFeesToProtocolVault(uint256 value) internal { + if (value == 0) return; + + (bool success,) = payable(generalConfig.protocolVault).call{value: value}(""); + if (!success) { + revert Errors.MultiVault_TransferFailed(); + } + + emit FeesTransferred(msg.sender, generalConfig.protocolVault, value); + } + + /// @dev _depositAtomFraction - divides amount across the three atoms composing the triple and issues shares to + /// the receiver. Doesn't charge additional protocol fees, but it does charge entry fees on each deposit + /// into an atom vault. + /// NOTE: assumes funds have already been transferred to this contract + function _depositAtomFraction(uint256 id, address receiver, uint256 amount) internal { + // load atom IDs + uint256[3] memory atomsIds; + (atomsIds[0], atomsIds[1], atomsIds[2]) = getTripleAtoms(id); + + // floor div, so perAtom is slightly less than 1/3 of total input amount + uint256 perAtom = amount / 3; + + // distribute proportional shares to each atom + for (uint256 i = 0; i < 3; i++) { + // deposit assets into each atom vault and mint shares for the receiver + uint256 shares = _deposit(receiver, atomsIds[i], perAtom); + + // update the mapping which tracks atom shares + tripleAtomShares[id][atomsIds[i]][receiver] += shares; + } + } + + /// @dev deposit assets into a vault upon creation + /// change the vault's total assets, total shares and balanceOf mappings to reflect the deposit + /// Additionally, initializes a counter vault with ghost shares. + function _depositOnVaultCreation(uint256 id, address receiver, uint256 assets) internal { + bool isAtomWallet = receiver == computeAtomWalletAddr(id); + + // ghost shares minted to the zero address upon vault creation + uint256 sharesForZeroAddress = generalConfig.minShare; + + // ghost shares for the counter vault + uint256 assetsForZeroAddressInCounterVault = generalConfig.minShare; + + uint256 sharesForReceiver = assets; + + // changes in vault's total assets or total shares + uint256 totalDelta = isAtomWallet ? sharesForReceiver : sharesForReceiver + sharesForZeroAddress; + + // set vault totals for the vault + _setVaultTotals(id, vaults[id].totalAssets + totalDelta, vaults[id].totalShares + totalDelta); + + // mint `sharesOwed` shares to sender factoring in fees + _mint(receiver, id, sharesForReceiver); + + // mint `sharesForZeroAddress` shares to zero address to initialize the vault + if (!isAtomWallet) { + _mint(address(0), id, sharesForZeroAddress); + } + + /* + * Initialize the counter triple vault with ghost shares if it is a triple creation flow + */ + if (isTripleId(id)) { + uint256 counterVaultId = getCounterIdFromTriple(id); + + // set vault totals + _setVaultTotals( + counterVaultId, + vaults[counterVaultId].totalAssets + assetsForZeroAddressInCounterVault, + vaults[counterVaultId].totalShares + sharesForZeroAddress + ); + + // mint `sharesForZeroAddress` shares to zero address to initialize the vault + _mint(address(0), counterVaultId, sharesForZeroAddress); + } + + emit Deposited( + msg.sender, + receiver, + vaults[id].balanceOf[receiver], + assets, // userAssetsAfterTotalFees + totalDelta, // sharesForReceiver + 0, // entryFee is not charged on vault creation + id + ); + } + + /// @notice Internal function to encapsulate the common deposit logic for both atoms and triples + /// @param receiver the address to receive the shares + /// @param id the vault ID of the atom or triple + /// @param value the amount of eth to deposit + /// @return sharesForReceiver the amount of shares minted + function _deposit(address receiver, uint256 id, uint256 value) internal returns (uint256) { + if (previewDeposit(value, id) == 0) { + revert Errors.MultiVault_DepositOrWithdrawZeroShares(); + } + + (uint256 totalAssetsDelta, uint256 sharesForReceiver, uint256 userAssetsAfterTotalFees, uint256 entryFee) = + getDepositSharesAndFees(value, id); + + if (totalAssetsDelta <= 0) { + revert Errors.MultiVault_InsufficientDepositAmountToCoverFees(); + } + + uint256 totalSharesDelta = sharesForReceiver; + + // set vault totals (assets and shares) + _setVaultTotals(id, vaults[id].totalAssets + totalAssetsDelta, vaults[id].totalShares + totalSharesDelta); + + // mint `sharesOwed` shares to sender factoring in fees + _mint(receiver, id, sharesForReceiver); + + emit Deposited( + msg.sender, + receiver, + vaults[id].balanceOf[receiver], + userAssetsAfterTotalFees, + sharesForReceiver, + entryFee, + id + ); + + return sharesForReceiver; + } + + /// @dev redeem shares out of a given vault + /// change the vault's total assets, total shares and balanceOf mappings to reflect the withdrawal + /// @return assetsForReceiver the amount of assets/eth to be transferred to the receiver + /// @return protocolFees the amount of protocol fees deducted + function _redeem(uint256 id, address owner, uint256 shares) internal returns (uint256, uint256) { + if (shares == 0) { + revert Errors.MultiVault_DepositOrWithdrawZeroShares(); + } + + if (vaults[id].balanceOf[msg.sender] < shares) { + revert Errors.MultiVault_InsufficientSharesInVault(); + } + + uint256 remainingShares = vaults[id].totalShares - shares; + if (remainingShares < generalConfig.minShare) { + revert Errors.MultiVault_InsufficientRemainingSharesInVault(remainingShares); + } + + (, uint256 assetsForReceiver, uint256 protocolFees, uint256 exitFees) = getRedeemAssetsAndFees(shares, id); + + // set vault totals (assets and shares) + _setVaultTotals( + id, + vaults[id].totalAssets - (assetsForReceiver + protocolFees), // totalAssetsDelta + vaults[id].totalShares - shares // totalSharesDelta + ); + + // burn shares, then transfer assets to receiver + _burn(owner, id, shares); + + emit Redeemed(msg.sender, owner, vaults[id].balanceOf[owner], assetsForReceiver, shares, exitFees, id); + + return (assetsForReceiver, protocolFees); + } + + /// @dev mint vault shares of vault ID `id` to address `to` + function _mint(address to, uint256 id, uint256 amount) internal { + vaults[id].balanceOf[to] += amount; + } + + /// @dev burn `amount` vault shares of vault ID `id` from address `from` + function _burn(address from, uint256 id, uint256 amount) internal { + if (from == address(0)) revert Errors.MultiVault_BurnFromZeroAddress(); + + uint256 fromBalance = vaults[id].balanceOf[from]; + if (fromBalance < amount) { + revert Errors.MultiVault_BurnInsufficientBalance(); + } + + unchecked { + vaults[id].balanceOf[from] = fromBalance - amount; + } + } + + /// @dev set total assets and shares for a vault + function _setVaultTotals(uint256 id, uint256 totalAssets, uint256 totalShares) internal { + vaults[id].totalAssets = totalAssets; + vaults[id].totalShares = totalShares; + } + + /// @dev internal method for vault creation + function _createVault() internal returns (uint256) { + uint256 id = ++count; + return id; + } + + /// @dev internal method to validate the timelock constraints + function _validateTimelock(bytes32 operationHash) internal view { + Timelock memory timelock = timelocks[operationHash]; + + if (timelock.readyTime == 0) { + revert Errors.MultiVault_OperationNotScheduled(); + } + if (timelock.executed) { + revert Errors.MultiVault_OperationAlreadyExecuted(); + } + if (timelock.readyTime > block.timestamp) { + revert Errors.MultiVault_TimelockNotExpired(); + } + } + + /* =================================================== */ + /* RESTRICTED FUNCTIONS */ + /* =================================================== */ + + /// @dev pause the pausable contract methods + function pause() external onlyAdmin { + _pause(); + } + + /// @dev unpause the pausable contract methods + function unpause() external onlyAdmin { + _unpause(); + } + + /// @dev schedule an operation to be executed after a delay + /// @param operationId unique identifier for the operation + /// @param data data to be executed + function scheduleOperation(bytes32 operationId, bytes calldata data) external onlyAdmin { + uint256 minDelay = generalConfig.minDelay; + + // Generate the operation hash + bytes32 operationHash = keccak256(abi.encodePacked(operationId, data, minDelay)); + + // Check timelock constraints and schedule the operation + if (timelocks[operationHash].readyTime != 0) { + revert Errors.MultiVault_OperationAlreadyScheduled(); + } + + // calculate the time when the operation can be executed + uint256 readyTime = block.timestamp + minDelay; + + timelocks[operationHash] = Timelock({data: data, readyTime: readyTime, executed: false}); + } + + /// @dev execute a scheduled operation + /// @param operationId unique identifier for the operation + /// @param data data to be executed + function cancelOperation(bytes32 operationId, bytes calldata data) external onlyAdmin { + // Generate the operation hash + bytes32 operationHash = keccak256(abi.encodePacked(operationId, data, generalConfig.minDelay)); + + // Check timelock constraints and cancel the operation + Timelock memory timelock = timelocks[operationHash]; + + if (timelock.readyTime == 0) { + revert Errors.MultiVault_OperationNotScheduled(); + } + if (timelock.executed) { + revert Errors.MultiVault_OperationAlreadyExecuted(); + } + + delete timelocks[operationHash]; + } + + /// @dev set admin + /// @param admin address of the new admin + function setAdmin(address admin) external onlyAdmin { + // Generate the operation hash + bytes memory data = abi.encodeWithSelector(EthMultiVault.setAdmin.selector, admin); + bytes32 opHash = keccak256(abi.encodePacked(SET_ADMIN, data, generalConfig.minDelay)); + + // Check timelock constraints + _validateTimelock(opHash); + + // Execute the operation + generalConfig.admin = admin; + + // Mark the operation as executed + timelocks[opHash].executed = true; + } + + /// @dev set protocol vault + /// @param protocolVault address of the new protocol vault + function setProtocolVault(address protocolVault) external onlyAdmin { + generalConfig.protocolVault = protocolVault; + } + + /// @dev sets the minimum deposit amount for atoms and triples + /// @param minDeposit new minimum deposit amount + function setMinDeposit(uint256 minDeposit) external onlyAdmin { + generalConfig.minDeposit = minDeposit; + } + + /// @dev sets the minimum share amount for atoms and triples + /// @param minShare new minimum share amount + function setMinShare(uint256 minShare) external onlyAdmin { + generalConfig.minShare = minShare; + } + + /// @dev sets the atom URI max length + /// @param atomUriMaxLength new atom URI max length + function setAtomUriMaxLength(uint256 atomUriMaxLength) external onlyAdmin { + generalConfig.atomUriMaxLength = atomUriMaxLength; + } + + /// @dev sets the atom share lock fee + /// @param atomWalletInitialDepositAmount new atom share lock fee + function setAtomWalletInitialDepositAmount(uint256 atomWalletInitialDepositAmount) external onlyAdmin { + atomConfig.atomWalletInitialDepositAmount = atomWalletInitialDepositAmount; + } + + /// @dev sets the atom creation fee + /// @param atomCreationProtocolFee new atom creation fee + function setAtomCreationProtocolFee(uint256 atomCreationProtocolFee) external onlyAdmin { + atomConfig.atomCreationProtocolFee = atomCreationProtocolFee; + } + + /// @dev sets fee charged in wei when creating a triple to protocol vault + /// @param tripleCreationProtocolFee new fee in wei + function setTripleCreationProtocolFee(uint256 tripleCreationProtocolFee) external onlyAdmin { + tripleConfig.tripleCreationProtocolFee = tripleCreationProtocolFee; + } + + /// @dev sets the atom deposit fraction percentage for atoms used in triples + /// (number to be divided by `generalConfig.feeDenominator`) + /// @param atomDepositFractionForTriple new atom deposit fraction percentage + function setAtomDepositFractionForTriple(uint256 atomDepositFractionForTriple) external onlyAdmin { + tripleConfig.atomDepositFractionForTriple = atomDepositFractionForTriple; + } + + /// @dev sets entry fees for the specified vault (id=0 sets the default fees for all vaults) + /// id = 0 changes the default entry fee, id = n changes fees for vault n specifically + /// @dev entry fee cannot be set greater than `generalConfig.feeDenominator` (which represents 100%) + /// @param id vault id to set entry fee for + /// @param entryFee entry fee to set + function setEntryFee(uint256 id, uint256 entryFee) external onlyAdmin { + if (entryFee > generalConfig.feeDenominator) { + revert Errors.MultiVault_InvalidFeeSet(); + } + + vaultFees[id].entryFee = entryFee; + } + + /// @dev sets exit fees for the specified vault (id=0 sets the default fees for all vaults) + /// id = 0 changes the default exit fee, id = n changes fees for vault n specifically + /// @dev admin cannot set the exit fee to be greater than `maxExitFeePercentage`, which is + /// set to be the 10% of `generalConfig.feeDenominator` (which represents 100%), to avoid + /// being able to prevent users from withdrawing their assets + /// @param id vault id to set exit fee for + /// @param exitFee exit fee to set + function setExitFee(uint256 id, uint256 exitFee) external onlyAdmin { + uint256 maxExitFeePercentage = generalConfig.feeDenominator / 10; + + if (exitFee > maxExitFeePercentage) { + revert Errors.MultiVault_InvalidExitFee(); + } + + // Generate the operation hash + bytes memory data = abi.encodeWithSelector(EthMultiVault.setExitFee.selector, id, exitFee); + bytes32 opHash = keccak256(abi.encodePacked(SET_EXIT_FEE, data, generalConfig.minDelay)); + + // Check timelock constraints + _validateTimelock(opHash); + + // Execute the operation + vaultFees[id].exitFee = exitFee; + + // Mark the operation as executed + timelocks[opHash].executed = true; + } + + /// @dev sets protocol fees for the specified vault (id=0 sets the default fees for all vaults) + /// id = 0 changes the default protocol fee, id = n changes fees for vault n specifically + /// @dev protocol fee cannot be set greater than `generalConfig.feeDenominator` (which represents 100%) + /// @param id vault id to set protocol fee for + /// @param protocolFee protocol fee to set + function setProtocolFee(uint256 id, uint256 protocolFee) external onlyAdmin { + if (protocolFee > generalConfig.feeDenominator) { + revert Errors.MultiVault_InvalidFeeSet(); + } + + vaultFees[id].protocolFee = protocolFee; + } + + /// @dev sets the atomWarden address + /// @param atomWarden address of the new atomWarden + function setAtomWarden(address atomWarden) external onlyAdmin { + walletConfig.atomWarden = atomWarden; + } + + /* =================================================== */ + /* MODIFIERS */ + /* =================================================== */ + + modifier onlyAdmin() { + if (msg.sender != generalConfig.admin) { + revert Errors.MultiVault_AdminOnly(); + } + _; + } + + /* =================================================== */ + /* FALLBACK */ + /* =================================================== */ + + /// @notice fallback function to decompress the calldata and call the appropriate function + fallback() external payable { + LibZip.cdFallback(); + } + + /// @notice contract does not accept ETH donations + receive() external payable { + revert Errors.MultiVault_ReceiveNotAllowed(); + } +} \ No newline at end of file diff --git a/src/interfaces/IEthMultiVault.sol b/src/interfaces/IEthMultiVault.sol new file mode 100644 index 0000000..a1f0164 --- /dev/null +++ b/src/interfaces/IEthMultiVault.sol @@ -0,0 +1,471 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.21; + +import {IPermit2} from "./IPermit2.sol"; + +/// @title IVaultManager +/// @notice Interface for managing many ERC4626 style vaults in a single contract +interface IEthMultiVault { + /* =================================================== */ + /* CONFIGS STRUCTS */ + /* =================================================== */ + + struct GeneralConfig { + /// @notice Admin address + address admin; + /// @notice Protocol vault address + address protocolVault; + /// @notice Fees are calculated by amount * (fee / feeDenominator); + uint256 feeDenominator; + /// @notice minimum amount of assets that must be deposited into an atom/triple vault + uint256 minDeposit; + /// @notice number of shares minted to zero address upon vault creation to initialize the vault + uint256 minShare; + /// @notice maximum length of the atom URI data that can be passed when creating atom vaults + uint256 atomUriMaxLength; + /// @notice decimal precision used for calculating share prices + uint256 decimalPrecision; + /// @notice minimum delay for timelocked transactions + uint256 minDelay; + } + + struct AtomConfig { + /// @notice fee charged for purchasing vault shares for the atom wallet + /// upon creation + uint256 atomWalletInitialDepositAmount; + /// @notice fee paid to the protocol when depositing vault shares for the atom vault upon creation + uint256 atomCreationProtocolFee; + } + + struct TripleConfig { + /// @notice fee paid to the protocol when depositing vault shares for the triple vault upon creation + uint256 tripleCreationProtocolFee; + /// @notice static fee going towards increasing the amount of assets in the underlying atom vaults + uint256 atomDepositFractionOnTripleCreation; + /// @notice % of the Triple deposit amount that is used to purchase equity in the underlying atoms + uint256 atomDepositFractionForTriple; + } + + struct WalletConfig { + /// @notice permit2 + IPermit2 permit2; + /// @notice Entry Point contract address used for the erc4337 atom accounts + address entryPoint; + /// @notice AtomWallet Warden address, address that is the initial owner of all atom accounts + address atomWarden; + /// @notice AtomWalletBeacon contract address, which points to the AtomWallet implementation + address atomWalletBeacon; + } + + /* =================================================== */ + /* OTHER STRUCTS */ + /* =================================================== */ + + struct VaultState { + uint256 totalAssets; + uint256 totalShares; + // address -> balanceOf, amount of shares an account has in a vault + mapping(address => uint256) balanceOf; + } + + struct VaultFees { + // entry fees are charged when depositing assets into the vault and they stay in the vault as assets + // rather than going towards minting shares for the recipient + // entry fee for vault 0 is considered the default entry fee + uint256 entryFee; + // exit fees are charged when redeeming shares from the vault and they stay in the vault as assets + // rather than being sent to the receiver + // exit fee for each vault, exit fee for vault 0 is considered the default exit fee + uint256 exitFee; + // protocol fees are charged both when depositing assets and redeeming shares from the vault and + // they are sent to the protocol vault address, as defined in `generalConfig.protocolVault` + // protocol fee for each vault, protocol fee for vault 0 is considered the default protocol fee + uint256 protocolFee; + } + + /// @notice Timelock struct + struct Timelock { + bytes data; + uint256 readyTime; + bool executed; + } + + /* =================================================== */ + /* EVENTS */ + /* =================================================== */ + + /// @notice Emitted upon the minting of shares in the vault by depositing assets + /// @param sender initializer of the deposit + /// @param receiver beneficiary of the minted shares + /// @param vaultBalance total assets held in the vault + /// @param userAssetsAfterTotalFees total assets that go towards minting shares for the receiver + /// @param sharesForReceiver total shares transferred + /// @param entryFee total fee amount collected for entering the vault + /// @param id vault id + event Deposited( + address indexed sender, + address indexed receiver, + uint256 vaultBalance, + uint256 userAssetsAfterTotalFees, + uint256 sharesForReceiver, + uint256 entryFee, + uint256 id + ); + + /// @notice Emitted upon the withdrawal of assets from the vault by redeeming shares + /// @param sender initializer of the withdrawal + /// @param owner owner of the shares that were redeemed + /// @param vaultBalance total assets held in the vault + /// @param assetsForReceiver quantity of assets withdrawn by the receiver + /// @param shares quantity of shares redeemed + /// @param exitFee total fee amount collected for exiting the vault + /// @param id vault id + event Redeemed( + address indexed sender, + address indexed owner, + uint256 vaultBalance, + uint256 assetsForReceiver, + uint256 shares, + uint256 exitFee, + uint256 id + ); + + /// @notice emitted upon creation of an atom + /// @param creator address of the atom creator + /// @param atomWallet address of the atom's associated abstract account + /// @param atomData the atom's respective string + /// @param vaultID the vault id of the atom + event AtomCreated(address indexed creator, address indexed atomWallet, bytes atomData, uint256 vaultID); + + /// @notice emitted upon creation of a triple + /// @param creator address of the triple creator + /// @param subjectId the triple's respective subject atom + /// @param predicateId the triple's respective predicate atom + /// @param objectId the triple's respective object atom + /// @param vaultID the vault id of the triple + event TripleCreated( + address indexed creator, uint256 subjectId, uint256 predicateId, uint256 objectId, uint256 vaultID + ); + + /// @notice emitted upon the transfer of fees to the protocol vault + /// @param sender address of the sender + /// @param protocolVault address of the protocol vault + /// @param amount amount of fees transferred + event FeesTransferred(address indexed sender, address indexed protocolVault, uint256 amount); + + /* =================================================== */ + /* FUNCTIONS */ + /* =================================================== */ + + /// @notice returns the cost of creating an atom + /// @return atomCost the cost of creating an atom + function getAtomCost() external view returns (uint256); + + /// @notice returns the cost of creating a triple + /// @return tripleCost the cost of creating a triple + function getTripleCost() external view returns (uint256); + + /// @notice returns the total fees that would be charged for depositing 'assets' into a vault + /// @param assets amount of `assets` to calculate fees on + /// @param id vault id to get corresponding fees for + /// @return totalFees total fees that would be charged for depositing 'assets' into a vault + function getDepositFees(uint256 assets, uint256 id) external view returns (uint256); + + /// @param assets amount of `assets` to calculate fees on (should always be msg.value - protocolFees) + /// @param id vault id to get corresponding fees for + /// @return totalAssetsDelta changes in vault's total assets + /// @return sharesForReceiver changes in vault's total shares (shares owed to receiver) + /// @return userAssetsAfterTotalFees amount of assets that goes towards minting shares for the receiver + /// @return entryFee amount of assets that would be charged for the entry fee + function getDepositSharesAndFees(uint256 assets, uint256 id) + external + view + returns (uint256, uint256, uint256, uint256); + + /// @notice returns the assets that would be returned to the receiver of the redeem and protocol fees + /// @param shares amount of `shares` to calculate fees on + /// @param id vault id to get corresponding fees for + /// @return totalUserAssets total amount of assets user would receive if redeeming 'shares', not including fees + /// @return assetsForReceiver amount of assets that is redeemable by the receiver + /// @return protocolFees amount of assets that would be sent to the protocol vault + /// @return exitFees amount of assets that would be charged for the exit fee + function getRedeemAssetsAndFees(uint256 shares, uint256 id) + external + view + returns (uint256, uint256, uint256, uint256); + + /// @notice returns amount of assets that would be charged for the entry fee given an amount of 'assets' provided + /// @param assets amount of assets to calculate fee on + /// @param id vault id to get corresponding fees for + /// @return feeAmount amount of assets that would be charged for the entry fee + /// NOTE: if the vault being deposited on has a vault total shares of 0, the entry fee is not applied + function entryFeeAmount(uint256 assets, uint256 id) external view returns (uint256); + + /// @notice returns amount of assets that would be charged for the exit fee given an amount of 'assets' provided + /// @param assets amount of assets to calculate fee on + /// @param id vault id to get corresponding fees for + /// @return feeAmount amount of assets that would be charged for the exit fee + /// NOTE: if the vault being redeemed from given the shares to redeem results in a total shares after of 0, + /// the exit fee is not applied + function exitFeeAmount(uint256 assets, uint256 id) external view returns (uint256); + + /// @notice returns amount of assets that would be charged by a vault on protocol fee given amount of 'assets' + /// provided + /// @param assets amount of assets to calculate fee on + /// @param id vault id to get corresponding fees for + /// @return feeAmount amount of assets that would be charged by vault on protocol fee + function protocolFeeAmount(uint256 assets, uint256 id) external view returns (uint256); + + /// @notice returns atom deposit fraction given amount of 'assets' provided + /// @param assets amount of assets to calculate fee on + /// @param id vault id + /// @return feeAmount amount of assets that would be used as atom deposit fraction + /// NOTE: only applies to triple vaults + function atomDepositFractionAmount(uint256 assets, uint256 id) external view returns (uint256); + + /// @notice Amount of shares that would be exchanged with the vault for the amount of assets provided + function convertToShares(uint256 assets, uint256 id) external view returns (uint256); + + /// @notice Amount of assets that would be exchanged with the vault for the amount of shares provided + function convertToAssets(uint256 shares, uint256 id) external view returns (uint256); + + /// @notice returns the current share price for the given vault id + /// @param id vault id to get corresponding share price for + /// @return price current share price for the given vault id + function currentSharePrice(uint256 id) external view returns (uint256); + + /// @notice Simulates the effects of depositing assets at the current block + function previewDeposit(uint256 assets, uint256 id) external view returns (uint256); + + /// @notice Simulates the effects of redeeming shares at the current block + function previewRedeem(uint256 shares, uint256 id) external view returns (uint256); + + /// @notice Max amount of shares that can be redeemed from the 'owner' balance through a redeem call + function maxRedeem(address owner, uint256 id) external view returns (uint256); + + /// @notice returns the corresponding hash for the given RDF triple, given the atoms that make up the triple + /// @param subjectId the subject atom's vault id + /// @param predicateId the predicate atom's vault id + /// @param objectId the object atom's vault id + /// @return hash the corresponding hash for the given RDF triple based on the atom vault ids + function tripleHashFromAtoms(uint256 subjectId, uint256 predicateId, uint256 objectId) + external + pure + returns (bytes32); + + /// @notice returns the corresponding hash for the given RDF triple, given the triple vault id + /// @param id vault id of the triple + /// @return hash the corresponding hash for the given RDF triple + /// NOTE: only applies to triple vault IDs as input + function tripleHash(uint256 id) external view returns (bytes32); + + /// @notice return true for triple vaults and false for atom vaults, designate if vault ID is a triple + /// @param id id of the vault inputted + function isTripleId(uint256 id) external view returns (bool); + + /// @notice return the underlying atom vault ids given a triple vault id + /// @param id Vault ID + function getTripleAtoms(uint256 id) external view returns (uint256, uint256, uint256); + + /// @notice returns the Triple ID for the given counter triple ID + /// @param id Counter Triple ID + function getCounterIdFromTriple(uint256 id) external returns (uint256); + + /// @notice mapping to designate if vault ID is a triple + /// @param id Vault ID + function isTriple(uint256 id) external view returns (bool); + + /// @notice return triple atom shares given triple id, atom id and account address + /// @param id Vault ID + /// @param atomId Id of the atom + /// @param account Address of the account + function tripleAtomShares(uint256 id, uint256 atomId, address account) external view returns (uint256); + + /// @notice returns the number of shares and assets (less fees) user has in the vault + /// @param vaultId vault id of the vault + /// @param receiver address of the receiver + /// @return shares number of shares user has in the vault + /// @return assets number of assets user has in the vault + function getVaultStateForUser(uint256 vaultId, address receiver) external view returns (uint256, uint256); + + /// @notice returns the Atom Wallet address for the given atom data + /// @param id vault id of the atom associated to the atom wallet + /// @return atomWallet the address of the atom wallet + /// NOTE: the create2 salt is based off of the vault ID + function computeAtomWalletAddr(uint256 id) external view returns (address); + + /// @notice returns the address of the atom warden + function getAtomWarden() external view returns (address); + + /// @notice deploy a given atom wallet + /// @param atomId vault id of atom + /// @return atomWallet the address of the atom wallet + /// NOTE: deploys an ERC4337 account (atom wallet) through a BeaconProxy. Reverts if the atom vault does not exist + function deployAtomWallet(uint256 atomId) external returns (address); + + /// @notice Initializes the MultiVault contract + /// @param _generalConfig General configuration struct + /// @param _atomConfig Atom configuration struct + /// @param _tripleConfig Triple configuration struct + /// @param _walletConfig Wallet configuration struct + /// @param _defaultVaultFees Default vault fees struct + /// @dev This function is called only once (during contract deployment) + function init( + GeneralConfig memory _generalConfig, + AtomConfig memory _atomConfig, + TripleConfig memory _tripleConfig, + WalletConfig memory _walletConfig, + VaultFees memory _defaultVaultFees + ) external; + + /// @notice Create an atom and return its vault id + /// @param atomUri atom data to create atom with + /// @return id vault id of the atom + /// NOTE: This function will revert if called with less than `getAtomCost()` in `msg.value` + function createAtom(bytes calldata atomUri) external payable returns (uint256); + + /// @notice Batch create atoms and return their vault ids + /// @param atomUris atom data array to create atoms with + /// @return ids vault ids array of the atoms + /// NOTE: This function will revert if called with less than `getAtomCost()` * `atomUris.length` in `msg.value` + function batchCreateAtom(bytes[] calldata atomUris) external payable returns (uint256[] memory); + + /// @notice create a triple and return its vault id + /// @param subjectId vault id of the subject atom + /// @param predicateId vault id of the predicate atom + /// @param objectId vault id of the object atom + /// @return id vault id of the triple + /// NOTE: This function will revert if called with less than `getTripleCost()` in `msg.value`. + /// This function will revert if any of the atoms do not exist or if any ids are triple vaults. + function createTriple(uint256 subjectId, uint256 predicateId, uint256 objectId) + external + payable + returns (uint256); + + /// @notice batch create triples and return their vault ids + /// @param subjectIds vault ids array of subject atoms + /// @param predicateIds vault ids array of predicate atoms + /// @param objectIds vault ids array of object atoms + /// NOTE: This function will revert if called with less than `getTripleCost()` * `array.length` in `msg.value`. + /// This function will revert if any of the atoms do not exist or if any ids are triple vaults. + function batchCreateTriple( + uint256[] calldata subjectIds, + uint256[] calldata predicateIds, + uint256[] calldata objectIds + ) external payable returns (uint256[] memory); + + /// @notice deposit eth into an atom vault and grant ownership of 'shares' to 'reciever' + /// *payable msg.value amount of eth to deposit + /// @dev assets parameter is omitted in favor of msg.value, unlike in ERC4626 + /// @param receiver the address to receive the shares + /// @param id the vault ID of the atom + /// @return shares the amount of shares minted + /// NOTE: this function will revert if the minimum deposit amount of eth is not met and + /// if the vault ID does not exist/is not an atom. + function depositAtom(address receiver, uint256 id) external payable returns (uint256); + + /// @notice redeem assets from an atom vault + /// @param shares the amount of shares to redeem + /// @param receiver the address to receiver the assets + /// @param id the vault ID of the atom + /// @return assets the amount of assets/eth withdrawn + function redeemAtom(uint256 shares, address receiver, uint256 id) external returns (uint256); + + /// @notice deposits assets of underlying tokens into a triple vault and grants ownership of 'shares' to 'reciever' + /// *payable msg.value amount of eth to deposit + /// @dev assets parameter is omitted in favor of msg.value, unlike in ERC4626 + /// @param receiver the address to receive the shares + /// @param id the vault ID of the triple + /// @return shares the amount of shares minted + /// NOTE: this function will revert if the minimum deposit amount of eth is not met and + /// if the vault ID does not exist/is not a triple. + function depositTriple(address receiver, uint256 id) external payable returns (uint256); + + /// @notice redeems 'shares' number of shares from the triple vault and send 'assets' eth + /// from the multiVault to 'reciever' factoring in exit fees + /// @param shares the amount of shares to redeem + /// @param receiver the address to receiver the assets + /// @param id the vault ID of the triple + /// @return assets the amount of assets/eth withdrawn + function redeemTriple(uint256 shares, address receiver, uint256 id) external returns (uint256); + + /// @dev pause the pausable contract methods + function pause() external; + + /// @dev unpause the pausable contract methods + function unpause() external; + + /// @dev schedule an operation to be executed after a delay + /// @param operationId unique identifier for the operation + /// @param data data to be executed + function scheduleOperation(bytes32 operationId, bytes calldata data) external; + + /// @dev execute a scheduled operation + /// @param operationId unique identifier for the operation + /// @param data data to be executed + function cancelOperation(bytes32 operationId, bytes calldata data) external; + + /// @dev set admin + /// @param admin address of the new admin + function setAdmin(address admin) external; + + /// @dev set protocol vault + /// @param protocolVault address of the new protocol vault + function setProtocolVault(address protocolVault) external; + + /// @dev sets the minimum deposit amount for atoms and triples + /// @param minDeposit new minimum deposit amount + function setMinDeposit(uint256 minDeposit) external; + + /// @dev sets the minimum share amount for atoms and triples + /// @param minShare new minimum share amount + function setMinShare(uint256 minShare) external; + + /// @dev sets the atom URI max length + /// @param atomUriMaxLength new atom URI max length + function setAtomUriMaxLength(uint256 atomUriMaxLength) external; + + /// @dev sets the atom share lock fee + /// @param atomWalletInitialDepositAmount new atom share lock fee + function setAtomWalletInitialDepositAmount(uint256 atomWalletInitialDepositAmount) external; + + /// @dev sets the atom creation fee + /// @param atomCreationProtocolFee new atom creation fee + function setAtomCreationProtocolFee(uint256 atomCreationProtocolFee) external; + + /// @dev sets fee charged in wei when creating a triple to protocol vault + /// @param tripleCreationProtocolFee new fee in wei + function setTripleCreationProtocolFee(uint256 tripleCreationProtocolFee) external; + + /// @dev sets the atom deposit fraction percentage for atoms used in triples + /// (number to be divided by `generalConfig.feeDenominator`) + /// @param atomDepositFractionForTriple new atom deposit fraction percentage + function setAtomDepositFractionForTriple(uint256 atomDepositFractionForTriple) external; + + /// @dev sets entry fees for the specified vault (id=0 sets the default fees for all vaults) + /// id = 0 changes the default entry fee, id = n changes fees for vault n specifically + /// @dev entry fee cannot be set greater than `generalConfig.feeDenominator` (which represents 100%) + /// @param id vault id to set entry fee for + /// @param entryFee entry fee to set + function setEntryFee(uint256 id, uint256 entryFee) external; + + /// @dev sets exit fees for the specified vault (id=0 sets the default fees for all vaults) + /// id = 0 changes the default exit fee, id = n changes fees for vault n specifically + /// @dev admin cannot set the exit fee to be greater than `maxExitFeePercentage`, which is + /// set to be the 10% of `generalConfig.feeDenominator` (which represents 100%), to avoid + /// being able to prevent users from withdrawing their assets + /// @param id vault id to set exit fee for + /// @param exitFee exit fee to set + function setExitFee(uint256 id, uint256 exitFee) external; + + /// @dev sets protocol fees for the specified vault (id=0 sets the default fees for all vaults) + /// id = 0 changes the default protocol fee, id = n changes fees for vault n specifically + /// @dev protocol fee cannot be set greater than `generalConfig.feeDenominator` (which represents 100%) + /// @param id vault id to set protocol fee for + /// @param protocolFee protocol fee to set + function setProtocolFee(uint256 id, uint256 protocolFee) external; + + /// @dev sets the atomWarden address + /// @param atomWarden address of the new atomWarden + function setAtomWarden(address atomWarden) external; +} \ No newline at end of file diff --git a/src/interfaces/IPermit2.sol b/src/interfaces/IPermit2.sol new file mode 100644 index 0000000..b102043 --- /dev/null +++ b/src/interfaces/IPermit2.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.21; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// Minimal Permit2 interface, derived from: +// https://github.com/Uniswap/permit2/blob/main/src/interfaces/ISignatureTransfer.sol +interface IPermit2 { + // Token and amount in a permit message. + struct TokenPermissions { + // Token to transfer. + IERC20 token; + // Amount to transfer. + uint256 amount; + } + + // The permit2 message. + struct PermitTransferFrom { + // Permitted token and amount. + TokenPermissions permitted; + // Unique identifier for this permit. + uint256 nonce; + // Expiration for this permit. + uint256 deadline; + } + + // Transfer details for permitTransferFrom(). + struct SignatureTransferDetails { + // Recipient of tokens. + address to; + // Amount to transfer. + uint256 requestedAmount; + } + + // Consume a permit2 message and transfer tokens. + function permitTransferFrom( + PermitTransferFrom calldata permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes calldata signature + ) external; +} \ No newline at end of file diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol new file mode 100644 index 0000000..6872a39 --- /dev/null +++ b/src/libraries/Errors.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.21; + +/// @title Errors Library +/// @notice Library containing all custom errors detailing cases in which the contracts should revert +library Errors { + /*//////////// MULTIVAULT ERRORS //////////////////////////////////////////////////////*/ + + error MultiVault_AdminOnly(); + error MultiVault_ArraysNotSameLength(); + error MultiVault_AtomDoesNotExist(uint256 atomId); + error MultiVault_AtomExists(bytes atomUri); + error MultiVault_AtomUriTooLong(); + error MultiVault_BurnFromZeroAddress(); + error MultiVault_BurnInsufficientBalance(); + error MultiVault_DeployAccountFailed(); + error MultiVault_DepositOrWithdrawZeroShares(); + error MultiVault_HasCounterStake(); + error MultiVault_InsufficientBalance(); + error MultiVault_InsufficientDepositAmountToCoverFees(); + error MultiVault_InsufficientRemainingSharesInVault(uint256 remainingShares); + error MultiVault_InsufficientSharesInVault(); + error MultiVault_InvalidExitFee(); + error MultiVault_InvalidFeeSet(); + error MultiVault_MinimumDeposit(); + error MultiVault_NoAtomWalletRewards(); + error MultiVault_OperationAlreadyExecuted(); + error MultiVault_OperationAlreadyScheduled(); + error MultiVault_OperationNotScheduled(); + error MultiVault_ReceiveNotAllowed(); + error MultiVault_TimelockNotExpired(); + error MultiVault_TransferFailed(); + error MultiVault_TripleExists(uint256 subjectId, uint256 predicateId, uint256 objectId); + error MultiVault_VaultDoesNotExist(); + error MultiVault_VaultIsTriple(uint256 vaultId); + error MultiVault_VaultNotAtom(); + error MultiVault_VaultNotTriple(); + + /*/////// ATOMWALLET ERRORS ///////////////////////////////////////////////////////////*/ + + error AtomWallet_OnlyOwner(); + error AtomWallet_OnlyOwnerOrEntryPoint(); + error AtomWallet_WrongArrayLengths(); +} \ No newline at end of file