// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol)
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC165 standard, as defined in the
* https://eips.ethereum.org/EIPS/eip-165[EIP].
*
* Implementers can declare support of contract interfaces, which can then be
* queried by others ({ERC165Checker}).
*
* For an implementation, see {ERC165}.
*/
interface IERC165 {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 amount) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `from` to `to` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/IERC721.sol)
pragma solidity ^0.8.0;
import "../../utils/introspection/IERC165.sol";
/**
* @dev Required interface of an ERC721 compliant contract.
*/
interface IERC721 is IERC165 {
/**
* @dev Emitted when `tokenId` token is transferred from `from` to `to`.
*/
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
*/
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
/**
* @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
*/
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
/**
* @dev Returns the number of tokens in ``owner``'s account.
*/
function balanceOf(address owner) external view returns (uint256 balance);
/**
* @dev Returns the owner of the `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function ownerOf(uint256 tokenId) external view returns (address owner);
/**
* @dev Safely transfers `tokenId` token from `from` to `to`.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
/**
* @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
* are aware of the ERC721 protocol to prevent tokens from being forever locked.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must exist and be owned by `from`.
* - If the caller is not `from`, it must have been allowed to move this token by either {approve} or {setApprovalForAll}.
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
*
* Emits a {Transfer} event.
*/
function safeTransferFrom(address from, address to, uint256 tokenId) external;
/**
* @dev Transfers `tokenId` token from `from` to `to`.
*
* WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC721
* or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must
* understand this adds an external call which potentially creates a reentrancy vulnerability.
*
* Requirements:
*
* - `from` cannot be the zero address.
* - `to` cannot be the zero address.
* - `tokenId` token must be owned by `from`.
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 tokenId) external;
/**
* @dev Gives permission to `to` to transfer `tokenId` token to another account.
* The approval is cleared when the token is transferred.
*
* Only a single account can be approved at a time, so approving the zero address clears previous approvals.
*
* Requirements:
*
* - The caller must own the token or be an approved operator.
* - `tokenId` must exist.
*
* Emits an {Approval} event.
*/
function approve(address to, uint256 tokenId) external;
/**
* @dev Approve or remove `operator` as an operator for the caller.
* Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
*
* Requirements:
*
* - The `operator` cannot be the caller.
*
* Emits an {ApprovalForAll} event.
*/
function setApprovalForAll(address operator, bool approved) external;
/**
* @dev Returns the account approved for `tokenId` token.
*
* Requirements:
*
* - `tokenId` must exist.
*/
function getApproved(uint256 tokenId) external view returns (address operator);
/**
* @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
*
* See {setApprovalForAll}
*/
function isApprovedForAll(address owner, address operator) external view returns (bool);
}
// SPDX-License-Identifier: unlicensed
// Cowri Labs Inc.
pragma solidity ^0.8.19;
/// @notice Implementing this allows a primitive to be called by the Ocean's
/// defi framework.
interface IOceanPrimitive {
function computeOutputAmount(uint256 inputToken, uint256 outputToken, uint256 inputAmount, address userAddress, bytes32 metadata) external returns (uint256 outputAmount);
function computeInputAmount(uint256 inputToken, uint256 outputToken, uint256 outputAmount, address userAddress, bytes32 metadata) external returns (uint256 inputAmount);
function getTokenSupply(uint256 tokenId) external view returns (uint256 totalSupply);
}
// SPDX-License-Identifier: unlicensed
// Cowri Labs Inc.
pragma solidity ^0.8.19;
/**
* @title Interface for external contracts that issue tokens on the Ocean's
* public multitoken ledger
* @dev Implemented by OceanERC1155.
*/
interface IOceanToken {
function registerNewTokens(uint256 currentNumberOfTokens, uint256 numberOfAdditionalTokens) external returns (uint256[] memory);
}
// SPDX-License-Identifier: MIT
// Cowri Labs Inc.
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
interface ISablierV2LockupLinear is IERC721 {
/// @notice Retrieves the address of the ERC-20 asset used for streaming.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream id for the query.
function getAsset(uint256 streamId) external view returns (IERC20 asset);
/// @notice Retrieves the amount deposited in the stream, denoted in units of the asset's decimals.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream id for the query.
function getDepositedAmount(uint256 streamId) external view returns (uint128 depositedAmount);
/// @notice Retrieves the stream's end time, which is a Unix timestamp.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream id for the query.
function getEndTime(uint256 streamId) external view returns (uint40 endTime);
/// @notice Retrieves the stream's recipient.
/// @dev Reverts if the NFT has been burned.
/// @param streamId The stream id for the query.
function getRecipient(uint256 streamId) external view returns (address recipient);
/// @notice Retrieves the amount refunded to the sender after a cancellation, denoted in units of the asset's
/// decimals. This amount is always zero unless the stream was canceled.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream id for the query.
function getRefundedAmount(uint256 streamId) external view returns (uint128 refundedAmount);
/// @notice Retrieves the stream's sender.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream id for the query.
function getSender(uint256 streamId) external view returns (address sender);
/// @notice Retrieves the stream's start time, which is a Unix timestamp.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream id for the query.
function getStartTime(uint256 streamId) external view returns (uint40 startTime);
/// @notice Retrieves the amount withdrawn from the stream, denoted in units of the asset's decimals.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream id for the query.
function getWithdrawnAmount(uint256 streamId) external view returns (uint128 withdrawnAmount);
/// @notice Retrieves a flag indicating whether the stream can be canceled. When the stream is cold, this
/// flag is always `false`.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream id for the query.
function isCancelable(uint256 streamId) external view returns (bool result);
/// @notice Retrieves a flag indicating whether the stream is cold, i.e. settled, canceled, or depleted.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream id for the query.
function isCold(uint256 streamId) external view returns (bool result);
/// @notice Retrieves a flag indicating whether the stream is depleted.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream id for the query.
function isDepleted(uint256 streamId) external view returns (bool result);
/// @notice Retrieves a flag indicating whether the stream exists.
/// @dev Does not revert if `streamId` references a null stream.
/// @param streamId The stream id for the query.
function isStream(uint256 streamId) external view returns (bool result);
/// @notice Retrieves a flag indicating whether the stream NFT can be transferred.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream id for the query.
function isTransferable(uint256 streamId) external view returns (bool result);
/// @notice Retrieves a flag indicating whether the stream is warm, i.e. either pending or streaming.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream id for the query.
function isWarm(uint256 streamId) external view returns (bool result);
/// @notice Counter for stream ids, used in the create functions.
function nextStreamId() external view returns (uint256);
/// @notice Calculates the amount that the sender would be refunded if the stream were canceled, denoted in units
/// of the asset's decimals.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream id for the query.
function refundableAmountOf(uint256 streamId) external view returns (uint128 refundableAmount);
/// @notice Calculates the amount streamed to the recipient, denoted in units of the asset's decimals.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream id for the query.
function streamedAmountOf(uint256 streamId) external view returns (uint128 streamedAmount);
/// @notice Retrieves a flag indicating whether the stream was canceled.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream id for the query.
function wasCanceled(uint256 streamId) external view returns (bool result);
/// @notice Calculates the amount that the recipient can withdraw from the stream, denoted in units of the asset's
/// decimals.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream id for the query.
function withdrawableAmountOf(uint256 streamId) external view returns (uint128 withdrawableAmount);
/*//////////////////////////////////////////////////////////////////////////
NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
/// @notice Burns the NFT associated with the stream.
///
/// @dev Emits a {Transfer} event.
///
/// Requirements:
/// - Must not be delegate called.
/// - `streamId` must reference a depleted stream.
/// - The NFT must exist.
/// - `msg.sender` must be either the NFT owner or an approved third party.
///
/// @param streamId The id of the stream NFT to burn.
function burn(uint256 streamId) external;
/// @notice Cancels the stream and refunds any remaining assets to the sender.
///
/// @dev Emits a {Transfer}, {CancelLockupStream}, and {MetadataUpdate} event.
///
/// Notes:
/// - If there any assets left for the recipient to withdraw, the stream is marked as canceled. Otherwise, the
/// stream is marked as depleted.
/// - This function attempts to invoke a hook on the recipient, if the resolved address is a contract.
///
/// Requirements:
/// - Must not be delegate called.
/// - The stream must be warm and cancelable.
/// - `msg.sender` must be the stream's sender.
///
/// @param streamId The id of the stream to cancel.
function cancel(uint256 streamId) external;
/// @notice Cancels multiple streams and refunds any remaining assets to the sender.
///
/// @dev Emits multiple {Transfer}, {CancelLockupStream}, and {MetadataUpdate} events.
///
/// Notes:
/// - Refer to the notes in {cancel}.
///
/// Requirements:
/// - All requirements from {cancel} must be met for each stream.
///
/// @param streamIds The ids of the streams to cancel.
function cancelMultiple(uint256[] calldata streamIds) external;
/// @notice Removes the right of the stream's sender to cancel the stream.
///
/// @dev Emits a {RenounceLockupStream} and {MetadataUpdate} event.
///
/// Notes:
/// - This is an irreversible operation.
/// - This function attempts to invoke a hook on the stream's recipient, provided that the recipient is a contract.
///
/// Requirements:
/// - Must not be delegate called.
/// - `streamId` must reference a warm stream.
/// - `msg.sender` must be the stream's sender.
/// - The stream must be cancelable.
///
/// @param streamId The id of the stream to renounce.
function renounce(uint256 streamId) external;
/// @notice Withdraws the provided amount of assets from the stream to the `to` address.
///
/// @dev Emits a {Transfer}, {WithdrawFromLockupStream}, and {MetadataUpdate} event.
///
/// Notes:
/// - This function attempts to invoke a hook on the stream's recipient, provided that the recipient is a contract
/// and `msg.sender` is either the sender or an approved operator.
///
/// Requirements:
/// - Must not be delegate called.
/// - `streamId` must not reference a null or depleted stream.
/// - `msg.sender` must be the stream's sender, the stream's recipient or an approved third party.
/// - `to` must be the recipient if `msg.sender` is the stream's sender.
/// - `to` must not be the zero address.
/// - `amount` must be greater than zero and must not exceed the withdrawable amount.
///
/// @param streamId The id of the stream to withdraw from.
/// @param to The address receiving the withdrawn assets.
/// @param amount The amount to withdraw, denoted in units of the asset's decimals.
function withdraw(uint256 streamId, address to, uint128 amount) external;
/// @notice Withdraws the maximum withdrawable amount from the stream to the provided address `to`.
///
/// @dev Emits a {Transfer}, {WithdrawFromLockupStream}, and {MetadataUpdate} event.
///
/// Notes:
/// - Refer to the notes in {withdraw}.
///
/// Requirements:
/// - Refer to the requirements in {withdraw}.
///
/// @param streamId The id of the stream to withdraw from.
/// @param to The address receiving the withdrawn assets.
function withdrawMax(uint256 streamId, address to) external;
/// @notice Withdraws the maximum withdrawable amount from the stream to the current recipient, and transfers the
/// NFT to `newRecipient`.
///
/// @dev Emits a {WithdrawFromLockupStream} and a {Transfer} event.
///
/// Notes:
/// - If the withdrawable amount is zero, the withdrawal is skipped.
/// - Refer to the notes in {withdraw}.
///
/// Requirements:
/// - `msg.sender` must be the stream's recipient.
/// - Refer to the requirements in {withdraw}.
/// - Refer to the requirements in {IERC721.transferFrom}.
///
/// @param streamId The id of the stream NFT to transfer.
/// @param newRecipient The address of the new owner of the stream NFT.
function withdrawMaxAndTransfer(uint256 streamId, address newRecipient) external;
/// @notice Withdraws assets from streams to the provided address `to`.
///
/// @dev Emits multiple {Transfer}, {WithdrawFromLockupStream}, and {MetadataUpdate} events.
///
/// Notes:
/// - This function attempts to call a hook on the recipient of each stream, unless `msg.sender` is the recipient.
///
/// Requirements:
/// - All requirements from {withdraw} must be met for each stream.
/// - There must be an equal number of `streamIds` and `amounts`.
///
/// @param streamIds The ids of the streams to withdraw from.
/// @param to The address receiving the withdrawn assets.
/// @param amounts The amounts to withdraw, denoted in units of the asset's decimals.
function withdrawMultiple(uint256[] calldata streamIds, address to, uint128[] calldata amounts) external;
}
// SPDX-License-Identifier: unlicensed
// Cowri Labs Inc.
pragma solidity ^0.8.19;
/**
* @param interactionTypeAndAddress the type of interaction and the external
* contract called during this interaction.
* @param inputToken this field is ignored except when the interaction type
* begins with "Compute". During a "Compute" interaction, this token is given
* to the external contract.
* @param outputToken this field is ignored except when the interaction type
* begins with "Compute". During a "Compute" interaction, this token is
* received from the external contract.
* @param specifiedAmount This value is the amount of the specified token.
* See the comment above the declaration for InteractionType for information
* on specified tokens. When this value is equal to type(uint256).max, it is
* a request by the user to use the intra-transaction delta of the specified
* token as the specified amount. See LibBalanceDelta for more information
* about this. When the Ocean executes an interaction, it resolves the
* specifiedAmount before calling the external contract. During a "721"
* interaction, the resolved specifiedAmount must be identically "1".
* @param metadata This value is used in two ways. During "Compute"
* interactions, it is forwarded to the external contract. The external
* contract can define whatever expectations it wants for these 32 bytes. The
* caller is expected to be aware of the expectations of the external contract
* invoked during the interaction. During 721/1155 and wraps and unwraps,
* these bytes are cast to uint256 and used as the external ledger's token ID
* for the interaction.
*/
struct Interaction {
bytes32 interactionTypeAndAddress;
uint256 inputToken;
uint256 outputToken;
uint256 specifiedAmount;
bytes32 metadata;
}
/**
* InteractionType determines how the properties of Interaction are interpreted
*
* The interface implemented by the external contract, the specified token
* for the interaction, and what sign (+/-) of delta can be used are
* determined by the InteractionType.
*
* @param WrapErc20
* type(externalContract).interfaceId == IERC20
* specifiedToken == calculateOceanId(externalContract, 0)
* negative delta can be used as specifiedAmount
*
* @param UnwrapErc20
* type(externalContract).interfaceId == IERC20
* specifiedToken == calculateOceanId(externalContract, 0)
* positive delta can be used as specifiedAmount
*
* @param WrapErc721
* type(externalContract).interfaceId == IERC721
* specifiedToken == calculateOceanId(externalContract, metadata)
* negative delta can be used as specifiedAmount
*
* @param UnwrapErc721
* type(externalContract).interfaceId == IERC721
* specifiedToken == calculateOceanId(externalContract, metadata)
* positive delta can be used as specifiedAmount
*
* @param WrapErc1155
* type(externalContract).interfaceId == IERC1155
* specifiedToken == calculateOceanId(externalContract, metadata)
* negative delta can be used as specifiedAmount
*
* @param WrapErc1155
* type(externalContract).interfaceId == IERC1155
* specifiedToken == calculateOceanId(externalContract, metadata)
* positive delta can be used as specifiedAmount
*
* @param ComputeInputAmount
* type(externalContract).interfaceId == IOceanexternalContract
* specifiedToken == outputToken
* negative delta can be used as specifiedAmount
*
* @param ComputeOutputAmount
* type(externalContract).interfaceId == IOceanexternalContract
* specifiedToken == inputToken
* positive delta can be used as specifiedAmount
*/
enum InteractionType {
WrapErc20,
UnwrapErc20,
WrapErc721,
UnwrapErc721,
WrapErc1155,
UnwrapErc1155,
ComputeInputAmount,
ComputeOutputAmount,
UnwrapEther
}
interface IOceanInteractions {
function unwrapFeeDivisor() external view returns (uint256);
function doMultipleInteractions(Interaction[] calldata interactions, uint256[] calldata ids) external payable returns (uint256[] memory burnIds, uint256[] memory burnAmounts, uint256[] memory mintIds, uint256[] memory mintAmounts);
function forwardedDoMultipleInteractions(
Interaction[] calldata interactions,
uint256[] calldata ids,
address userAddress
)
external
payable
returns (uint256[] memory burnIds, uint256[] memory burnAmounts, uint256[] memory mintIds, uint256[] memory mintAmounts);
function doInteraction(Interaction calldata interaction) external payable returns (uint256 burnId, uint256 burnAmount, uint256 mintId, uint256 mintAmount);
function forwardedDoInteraction(Interaction calldata interaction, address userAddress) external payable returns (uint256 burnId, uint256 burnAmount, uint256 mintId, uint256 mintAmount);
}
// SPDX-License-Identifier: Unlicensed
pragma solidity ^0.8.19;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IOceanPrimitive } from "../interfaces/IOceanPrimitive.sol";
import { IOceanToken } from "../interfaces/IOceanToken.sol";
import "../interfaces/Interactions.sol";
import "../interfaces/ISablierV2LockupLinear.sol";
/**
* @notice
* Allows vesting token owners to fractionalize their tokens
*
* @dev
* Inherits from -
* IOceanPrimitive: This is an Ocean primitive and hence the methods can only be accessed by the Ocean contract.
*/
contract VestingFractionalizer is IOceanPrimitive {
//*********************************************************************//
// --------------------------- custom errors ------------------------- //
//*********************************************************************//
error UNAUTHORIZED();
error INVALID_AMOUNT();
error INVALID_TOKEN_ID();
error INVALID_VESTING_STREAM();
//*********************************************************************//
// --------------- public immutable stored properties ---------------- //
//*********************************************************************//
/**
* @notice
* ocean contract address.
*/
IOceanInteractions public immutable ocean;
/**
* @notice
* sablier lock linear stream contract.
*/
ISablierV2LockupLinear public immutable lockupLinear;
/**
* @notice
* shell token address.
*/
IERC20 public immutable shell;
/**
* @notice
* stream creator address.
*/
address public immutable streamCreator;
/**
* @notice
* fungible token id
*/
uint256 public immutable fungibleTokenId;
/**
* @notice
* end time for filtering valid vesting streams
*/
uint256 public immutable streamEndTime;
/**
* @notice
* fungible token total supply
*/
uint256 public fungibleTokenSupply;
//*********************************************************************//
// ---------------------------- constructor -------------------------- //
//*********************************************************************//
constructor(IOceanInteractions ocean_, ISablierV2LockupLinear lockupLinear_, IERC20 shell_, address _streamCreator, uint256 _streamEndTime) {
ocean = ocean_;
lockupLinear = lockupLinear_;
shell = shell_;
streamCreator = _streamCreator;
streamEndTime = _streamEndTime;
uint256[] memory registeredToken = IOceanToken(address(ocean_)).registerNewTokens(0, 1);
fungibleTokenId = registeredToken[0];
lockupLinear.setApprovalForAll(address(ocean_), true);
}
/**
* @notice Modifier to make sure msg.sender is the Ocean contract.
*/
modifier onlyOcean() {
if (msg.sender != address(ocean)) revert UNAUTHORIZED();
_;
}
function _withdrawMax(uint256 _id, address _user) internal {
lockupLinear.withdrawMax(_id, _user);
}
/**
* @dev wraps the underlying token into the Ocean
*/
function wrapERC721(uint256 _tokenId) internal {
Interaction memory interaction =
Interaction({ interactionTypeAndAddress: _fetchInteractionId(address(lockupLinear), uint256(InteractionType.WrapErc721)), inputToken: 0, outputToken: 0, specifiedAmount: 1, metadata: bytes32(_tokenId) });
ocean.doInteraction(interaction);
}
/**
* @dev unwraps the underlying token from the Ocean
*/
function unwrapERC721(uint256 _tokenId) internal {
Interaction memory interaction =
Interaction({ interactionTypeAndAddress: _fetchInteractionId(address(lockupLinear), uint256(InteractionType.UnwrapErc721)), inputToken: 0, outputToken: 0, specifiedAmount: 1, metadata: bytes32(_tokenId) });
ocean.doInteraction(interaction);
}
/**
* @notice
* Calculates the output amount for a specified input amount.
*
* @param inputToken Input token id
* @param outputToken Output token id
* @param inputAmount Input token amount
* @param tokenId erc721 token id
*
* @return outputAmount Output amount
*/
function computeOutputAmount(uint256 inputToken, uint256 outputToken, uint256 inputAmount, address user, bytes32 tokenId) external override onlyOcean returns (uint256 outputAmount) {
uint256 nftOceanId = _calculateOceanId(address(lockupLinear), uint256(tokenId));
if (inputToken == nftOceanId && outputToken == fungibleTokenId) {
if (inputAmount != 1) revert INVALID_AMOUNT();
// valid stream nft's that can be fractionalized must adhere to all the following criteria's
// 1. the sender should be the streamCreator
// 2. the asset should be shell address
// 3. the end time should be the streamEndTime
// 4. the stream should be non-cancellable
if (lockupLinear.getSender(uint256(tokenId)) != streamCreator || lockupLinear.getAsset(uint256(tokenId)) != shell || lockupLinear.getEndTime(uint256(tokenId)) != streamEndTime || lockupLinear.isCancelable(uint256(tokenId))) revert INVALID_VESTING_STREAM();
// unwrap the nft
unwrapERC721(uint256(tokenId));
// withdraw underlying tokens from the stream
_withdrawMax(uint256(tokenId), user);
// wrap the nft again
wrapERC721(uint256(tokenId));
// set the fungible amount to mint as the remaining underlying amount that is still vesting
outputAmount = lockupLinear.getDepositedAmount(uint256(tokenId)) - lockupLinear.getWithdrawnAmount(uint256(tokenId));
fungibleTokenSupply += outputAmount;
} else if (inputToken == fungibleTokenId && outputToken == nftOceanId) {
// revert if the total underlying amount isn't the input amount
uint256 _totalUnderlyingTokenAmount = lockupLinear.getDepositedAmount(uint256(tokenId)) - lockupLinear.getWithdrawnAmount(uint256(tokenId));
if (_totalUnderlyingTokenAmount != inputAmount) revert INVALID_AMOUNT();
fungibleTokenSupply -= inputAmount;
outputAmount = 1;
} else {
revert("Invalid input and output tokens");
}
}
/**
* @notice
* Calculates the input amount for a specified output amount
*
* @param inputToken Input token id
* @param outputToken Output token id
* @param outputAmount Output token amount
* @param tokenId erc721 token id.
*
* @return inputAmount Input amount
*/
function computeInputAmount(uint256 inputToken, uint256 outputToken, uint256 outputAmount, address user, bytes32 tokenId) external override onlyOcean returns (uint256 inputAmount) {
uint256 nftOceanId = _calculateOceanId(address(lockupLinear), uint256(tokenId));
if (inputToken == fungibleTokenId && outputToken == nftOceanId) {
if (outputAmount != 1) revert INVALID_AMOUNT();
uint256 _withdrawableAmount = lockupLinear.getDepositedAmount(uint256(tokenId)) - lockupLinear.getWithdrawnAmount(uint256(tokenId));
inputAmount = _withdrawableAmount;
fungibleTokenSupply -= inputAmount;
} else revert();
}
/**
* @notice
* Get total fungible supply
*
* @param tokenId Fungible token id
* @return totalSupply Current total supply.
*/
function getTokenSupply(uint256 tokenId) external view override returns (uint256 totalSupply) {
if (tokenId != fungibleTokenId) revert INVALID_TOKEN_ID();
totalSupply = fungibleTokenSupply;
}
/**
* @notice
* Get Ocean token id
*
* @param tokenContract NFT collection contract
* @return tokenId erc721 token id.
*/
function _calculateOceanId(address tokenContract, uint256 tokenId) internal pure returns (uint256) {
return uint256(keccak256(abi.encodePacked(tokenContract, tokenId)));
}
/**
* @notice used to fetch the Ocean interaction ID
*/
function _fetchInteractionId(address token, uint256 interactionType) internal pure returns (bytes32) {
uint256 packedValue = uint256(uint160(token));
packedValue |= interactionType << 248;
return bytes32(abi.encode(packedValue));
}
/**
* @dev Handles the receipt of a single ERC1155 token type. This function is
* called at the end of a `safeTransferFrom` after the balance has been updated.
* @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed
*/
function onERC1155Received(address, address, uint256, uint256, bytes memory) public pure returns (bytes4) {
return this.onERC1155Received.selector;
}
function onERC721Received(address, address, uint256, bytes calldata) public pure returns (bytes4) {
return this.onERC721Received.selector;
}
}
{
"compilationTarget": {
"src/fractionalizer/VestingFractionalizer.sol": "VestingFractionalizer"
},
"evmVersion": "paris",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 10000000
},
"remappings": []
}
[{"inputs":[{"internalType":"contract IOceanInteractions","name":"ocean_","type":"address"},{"internalType":"contract ISablierV2LockupLinear","name":"lockupLinear_","type":"address"},{"internalType":"contract IERC20","name":"shell_","type":"address"},{"internalType":"address","name":"_streamCreator","type":"address"},{"internalType":"uint256","name":"_streamEndTime","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"INVALID_AMOUNT","type":"error"},{"inputs":[],"name":"INVALID_TOKEN_ID","type":"error"},{"inputs":[],"name":"INVALID_VESTING_STREAM","type":"error"},{"inputs":[],"name":"UNAUTHORIZED","type":"error"},{"inputs":[{"internalType":"uint256","name":"inputToken","type":"uint256"},{"internalType":"uint256","name":"outputToken","type":"uint256"},{"internalType":"uint256","name":"outputAmount","type":"uint256"},{"internalType":"address","name":"user","type":"address"},{"internalType":"bytes32","name":"tokenId","type":"bytes32"}],"name":"computeInputAmount","outputs":[{"internalType":"uint256","name":"inputAmount","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"inputToken","type":"uint256"},{"internalType":"uint256","name":"outputToken","type":"uint256"},{"internalType":"uint256","name":"inputAmount","type":"uint256"},{"internalType":"address","name":"user","type":"address"},{"internalType":"bytes32","name":"tokenId","type":"bytes32"}],"name":"computeOutputAmount","outputs":[{"internalType":"uint256","name":"outputAmount","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"fungibleTokenId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"fungibleTokenSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getTokenSupply","outputs":[{"internalType":"uint256","name":"totalSupply","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lockupLinear","outputs":[{"internalType":"contract ISablierV2LockupLinear","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ocean","outputs":[{"internalType":"contract IOceanInteractions","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC1155Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC721Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"shell","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"streamCreator","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"streamEndTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]