diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/nest/src/token/DinariAdapterToken.sol b/nest/src/token/DinariAdapterToken.sol new file mode 100644 index 0000000..b1a857f --- /dev/null +++ b/nest/src/token/DinariAdapterToken.sol @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { ERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; + +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { DoubleEndedQueue } from "@openzeppelin/contracts/utils/DoubleEndedQueue.sol"; + +import { ComponentToken } from "../ComponentToken.sol"; +import { IAggregateToken } from "../interfaces/IAggregateToken.sol"; +import { IOrderProcessor } from "./external/IOrderProcessor.sol"; + +/** + * @title DinariAdapterToken + * @author Jake Timothy, Eugene Y. Q. Shen + * @notice Implementation of the abstract ComponentToken that interfaces with external assets. + * @dev Assets is USDC + */ +contract DinariAdapterToken is ComponentToken { + + using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; + + // Storage + + struct DShareOrderInfo { + bool sell; + uint256 orderAmount; + uint256 fees; + } + + /// @custom:storage-location erc7201:plume.storage.DinariAdapterToken + struct DinariAdapterTokenStorage { + /// @dev dShare token underlying component token + address dshareToken; + /// @dev Wrapped dShare token underlying component token + address wrappedDshareToken; + /// @dev Address of the Nest Staking contract + address nestStakingContract; + /// @dev Address of the dShares order contract + IOrderProcessor externalOrderContract; + // + mapping(uint256 orderId => DShareOrderInfo) submittedOrderInfo; + DoubleEndedQueue.Bytes32Deque submittedOrders; + } + + // keccak256(abi.encode(uint256(keccak256("plume.storage.DinariAdapterToken")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant DINARI_ADAPTER_TOKEN_STORAGE_LOCATION = + 0x2a49a1f589de6263f42d4846b2f178279aaa9b9efbd070fd2367cbda9b826400; + + function _getDinariAdapterTokenStorage() private pure returns (DinariAdapterTokenStorage storage $) { + assembly { + $.slot := DINARI_ADAPTER_TOKEN_STORAGE_LOCATION + } + } + + // Errors + + /** + * @notice Indicates a failure because the caller is not the authorized caller + * @param invalidCaller Address of the caller that is not the authorized caller + * @param caller Address of the authorized caller + */ + error Unauthorized(address invalidCaller, address caller); + + error NoOutstandingOrders(); + error OrderDoesNotExist(); + error OrderStillActive(); + + // Initializer + + /** + * @notice Prevent the implementation contract from being initialized or reinitialized + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor() { + _disableInitializers(); + } + + /** + * @notice Initialize the DinariAdapterToken + * @param owner Address of the owner of the DinariAdapterToken + * @param name Name of the DinariAdapterToken + * @param symbol Symbol of the DinariAdapterToken + * @param currencyToken CurrencyToken used to mint and burn the DinariAdapterToken + * @param dshareToken dShare token underlying component token + * @param decimals_ Number of decimals of the DinariAdapterToken + * @param nestStakingContract Address of the Nest Staking contract + * @param externalOrderContract Address of the dShares order contract + */ + function initialize( + address owner, + string memory name, + string memory symbol, + address currencyToken, + address dshareToken, + address wrappedDshareToken, + uint8 decimals_, + address nestStakingContract, + address externalOrderContract + ) public initializer { + super.initialize(owner, name, symbol, IERC20(currencyToken), decimals_); + DinariAdapterTokenStorage storage $ = _getDinariAdapterTokenStorage(); + $.dshareToken = dshareToken; + $.wrappedDshareToken = wrappedDshareToken; + $.nestStakingContract = nestStakingContract; + $.externalOrderContract = IOrderProcessor(externalOrderContract); + } + + // Override Functions + + /// @inheritdoc IERC4626 + function convertToShares( + uint256 assets + ) public view override(ComponentToken) returns (uint256 shares) { + // Apply dshare price and wrapped conversion rate, fees + DinariAdapterTokenStorage storage $ = _getDinariAdapterTokenStorage(); + IOrderProcessor orderContract = $.externalOrderContract; + address paymentToken = _getComponentTokenStorage().currencyToken; + (uint256 orderAmount, uint256 fees) = _getOrderFromTotalBuy(orderContract, paymentToken, assets); + IOrderProcessor.PricePoint memory price = orderContract.latestFillPrice($.dshareToken, paymentToken); + return IERC4626($.wrappedDshareToken).convertToShares(((orderAmount + fees) * price.price) / 1 ether); + } + + function _getOrderFromTotalBuy( + IOrderProcessor orderContract, + address paymentToken, + uint256 totalBuy + ) private view returns (uint256 orderAmount, uint256 fees) { + // order * (1 + vfee) + flat = total + // order = (total - flat) / (1 + vfee) + (uint256 flatFee, uint24 percentageFeeRate) = orderContract.getStandardFees(false, paymentToken); + orderAmount = (totalBuy - flatFee) * 1_000_000 / (1_000_000 + percentageFeeRate); + + fees = orderContract.totalStandardFee(false, paymentToken, orderAmount); + } + + /// @inheritdoc IERC4626 + function convertToAssets( + uint256 shares + ) public view override(ComponentToken) returns (uint256 assets) { + // Apply wrapped conversion rate and dshare price, subtract fees + DinariAdapterTokenStorage storage $ = _getDinariAdapterTokenStorage(); + IOrderProcessor orderContract = $.externalOrderContract; + address paymentToken = _getComponentTokenStorage().currencyToken; + address dshareToken = $.dshareToken; + IOrderProcessor.PricePoint memory price = orderContract.latestFillPrice(dshareToken, paymentToken); + uint256 dshares = IERC4626($.wrappedDshareToken).convertToAssets(shares); + // Round down to nearest supported decimal + uint256 precisionReductionFactor = 10 ** orderContract.orderDecimalReduction(dshareToken); + uint256 proceeds = ((dshares / precisionReductionFactor) * precisionReductionFactor * 1 ether) / price.price; + uint256 fees = orderContract.totalStandardFee(true, paymentToken, proceeds); + return proceeds - fees; + } + + /// @inheritdoc IComponentToken + function requestDeposit( + uint256 assets, + address controller, + address owner + ) public override(ComponentToken) returns (uint256 requestId) { + DinariAdapterTokenStorage storage $ = _getDinariAdapterTokenStorage(); + address nestStakingContract = $.nestStakingContract; + if (msg.sender != nestStakingContract) { + revert Unauthorized(msg.sender, nestStakingContract); + } + + IOrderProcessor orderContract = $.externalOrderContract; + address paymentToken = _getComponentTokenStorage().currencyToken; + (uint256 orderAmount, uint256 fees) = _getOrderFromTotalBuy(orderContract, paymentToken, assets); + uint256 totalInput = orderAmount + fees; + + // Subcall with calculated input amount to be safe + requestId = super.requestDeposit(totalInput, controller, owner); + + // Approve dshares + IERC20(paymentToken).approve(address(orderContract), totalInput); + // Buy + IOrderProcessor.Order memory order = IOrderProcessor.Order({ + requestTimestamp: block.timestamp, + recipient: address(this), + assetToken: $.dshareToken, + paymentToken: paymentToken, + sell: false, + orderType: IOrderProcessor.OrderType.MARKET, + assetTokenQuantity: 0, + paymentTokenQuantity: orderAmount, + price: 0, + tif: IOrderProcessor.TIF.DAY + }); + uint256 orderId = orderContract.createOrderStandardFees(order); + $.submittedOrderInfo[orderId] = DShareOrderInfo({ sell: false, orderAmount: orderAmount, fees: fees }); + $.submittedOrders.pushBack(bytes32(orderId)); + } + + /// @inheritdoc IComponentToken + function requestRedeem( + uint256 shares, + address controller, + address owner + ) public override(ComponentToken) returns (uint256 requestId) { + DinariAdapterTokenStorage storage $ = _getDinariAdapterTokenStorage(); + address nestStakingContract = $.nestStakingContract; + if (msg.sender != nestStakingContract) { + revert Unauthorized(msg.sender, nestStakingContract); + } + + // Unwrap dshares + address wrappedDshareToken = $.wrappedDshareToken; + uint256 dshares = IERC4626(wrappedDshareToken).redeem(shares); + // Round down to nearest supported decimal + address dshareToken = $.dshareToken; + uint256 precisionReductionFactor = 10 ** orderContract.orderDecimalReduction(dshareToken); + uint256 orderAmount = (dshares / precisionReductionFactor) * precisionReductionFactor; + + // Subcall with dust removed + requestId = super.requestRedeem(orderAmount, controller, owner); + + // Rewrap dust + uint256 dshareDust = dshares - orderAmount; + if (dshareDust > 0) { + IERC4626(wrappedDshareToken).deposit(dshareDust, address(this)); + } + // Approve dshares + IOrderProcessor orderContract = $.externalOrderContract; + IERC20(dshareToken).approve(address(orderContract), orderAmount); + // Sell + IOrderProcessor.Order memory order = IOrderProcessor.Order({ + requestTimestamp: block.timestamp, + recipient: address(this), + assetToken: dshareToken, + paymentToken: _getComponentTokenStorage().currencyToken, + sell: true, + orderType: IOrderProcessor.OrderType.MARKET, + assetTokenQuantity: orderAmount, + paymentTokenQuantity: 0, + price: 0, + tif: IOrderProcessor.TIF.DAY + }); + uint256 orderId = orderContract.createOrderStandardFees(order); + $.submittedOrderInfo[orderId] = DShareOrderInfo({ sell: true, orderAmount: orderAmount, fees: 0 }); + $.submittedOrders.pushBack(bytes32(orderId)); + } + + /// @dev Panic + function getNextSubmittedOrderStatus() public view returns (IOrderProcessor.OrderStatus) { + DinariAdapterTokenStorage storage $ = _getDinariAdapterTokenStorage(); + if ($.submittedOrders.length() == 0) { + revert NoOutstandingOrders(); + } + uint256 orderId = uint256($.submittedOrders.front()); + return $.externalOrderContract.getOrderStatus(orderId); + } + + function processSubmittedOrders() public { + DinariAdapterTokenStorage storage $ = _getDinariAdapterTokenStorage(); + IOrderProcessor orderContract = $.externalOrderContract; + address paymentToken = _getComponentTokenStorage().currencyToken; + address nestStakingContract = $.nestStakingContract; + while ($.submittedOrders.length() > 0) { + uint256 orderId = uint256($.submittedOrders.front()); + IOrderProcessor.OrderStatus status = orderContract.getOrderStatus(orderId); + if (status == IOrderProcessor.OrderStatus.ACTIVE) { + break; + } else if (status == IOrderProcessor.OrderStatus.NONE) { + revert OrderDoesNotExist(); + } + + DShareOrderInfo memory orderInfo = $.submittedOrderInfo[orderId]; + uint256 totalInput = orderInfo.orderAmount + orderInfo.fees; + + if (status == IOrderProcessor.OrderStatus.CANCELLED) { + // Assets have been refunded + $.pendingDepositRequest[controller] -= totalInput; + } else if (status == IOrderProcessor.OrderStatus.FULFILLED) { + uint256 proceeds = orderContract.getReceivedAmount(orderId); + + if (orderInfo.sell) { + super.notifyRedeem(proceeds, orderInfo.orderAmount, nestStakingContract); + } else { + super.notifyDeposit(totalInput, proceeds, nestStakingContract); + + // Send fee refund to controller + uint256 totalSpent = orderInfo.orderAmount + orderContract.getFeesTaken(orderId); + uint256 refund = totalInput - totalSpent; + if (refund > 0) { + IERC20(paymentToken).transfer(nestStakingContract, refund); + } + } + } + + $.submittedOrders.popFront(); + } + } + + /// @dev Single order processing if gas limit is reached + function processNextSubmittedOrder() public { + DinariAdapterTokenStorage storage $ = _getDinariAdapterTokenStorage(); + IOrderProcessor orderContract = $.externalOrderContract; + address nestStakingContract = $.nestStakingContract; + + if ($.submittedOrders.length() == 0) { + revert NoOutstandingOrders(); + } + uint256 orderId = uint256($.submittedOrders.front()); + IOrderProcessor.OrderStatus status = orderContract.getOrderStatus(orderId); + if (status == IOrderProcessor.OrderStatus.ACTIVE) { + revert OrderStillActive(); + } else if (status == IOrderProcessor.OrderStatus.NONE) { + revert OrderDoesNotExist(); + } + + DShareOrderInfo memory orderInfo = $.submittedOrderInfo[orderId]; + uint256 totalInput = orderInfo.orderAmount + orderInfo.fees; + + if (status == IOrderProcessor.OrderStatus.CANCELLED) { + // Assets have been refunded + $.pendingDepositRequest[controller] -= totalInput; + } else if (status == IOrderProcessor.OrderStatus.FULFILLED) { + uint256 proceeds = orderContract.getReceivedAmount(orderId); + + if (orderInfo.sell) { + super.notifyRedeem(proceeds, orderInfo.orderAmount, nestStakingContract); + } else { + super.notifyDeposit(totalInput, proceeds, nestStakingContract); + + // Send fee refund to controller + uint256 totalSpent = orderInfo.orderAmount + orderContract.getFeesTaken(orderId); + uint256 refund = totalInput - totalSpent; + if (refund > 0) { + IERC20(_getComponentTokenStorage().currencyToken).transfer(nestStakingContract, refund); + } + } + } + + $.submittedOrders.popFront(); + } + + /// @inheritdoc IComponentToken + function deposit( + uint256 assets, + address receiver, + address controller + ) public override(ComponentToken) returns (uint256 shares) { + AdapterTokenStorage storage $ = _getAdapterTokenStorage(); + if (msg.sender != address($.externalContract)) { + revert Unauthorized(msg.sender, address($.externalContract)); + } + if (receiver != address($.nestStakingContract)) { + revert Unauthorized(receiver, address($.nestStakingContract)); + } + return super.deposit(assets, receiver, controller); + } + + /// @inheritdoc IComponentToken + function redeem( + uint256 shares, + address receiver, + address controller + ) public override(ComponentToken) returns (uint256 assets) { + AdapterTokenStorage storage $ = _getAdapterTokenStorage(); + if (msg.sender != address($.externalContract)) { + revert Unauthorized(msg.sender, address($.externalContract)); + } + if (receiver != address($.nestStakingContract)) { + revert Unauthorized(receiver, address($.nestStakingContract)); + } + return super.redeem(shares, receiver, controller); + } + +} diff --git a/nest/src/token/external/IOrderProcessor.sol b/nest/src/token/external/IOrderProcessor.sol new file mode 100644 index 0000000..3bea337 --- /dev/null +++ b/nest/src/token/external/IOrderProcessor.sol @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +/// @notice Interface for contracts processing orders for dShares +/// @author Dinari (https://github.com/dinaricrypto/sbt-contracts/blob/main/src/orders/IOrderProcessor.sol) +/// This interface provides a standard Order type and order lifecycle events +/// Orders are requested on-chain, processed off-chain, then fulfillment is submitted for on-chain settlement +interface IOrderProcessor { + + /// ------------------ Types ------------------ /// + + // Market or limit order + enum OrderType { + MARKET, + LIMIT + } + + // Time in force + enum TIF { + // Good until end of day + DAY, + // Good until cancelled + GTC, + // Immediate or cancel + IOC, + // Fill or kill + FOK + } + + // Order status enum + enum OrderStatus { + // Order has never existed + NONE, + // Order is active + ACTIVE, + // Order is completely filled + FULFILLED, + // Order is cancelled + CANCELLED + } + + struct Order { + // Timestamp or other salt added to order hash for replay protection + uint64 requestTimestamp; + // Recipient of order fills + address recipient; + // Bridged asset token + address assetToken; + // Payment token + address paymentToken; + // Buy or sell + bool sell; + // Market or limit + OrderType orderType; + // Amount of asset token to be used for fills + uint256 assetTokenQuantity; + // Amount of payment token to be used for fills + uint256 paymentTokenQuantity; + // Price for limit orders in ether decimals + uint256 price; + // Time in force + TIF tif; + } + + struct OrderRequest { + // Unique ID and hash of order data used to validate order details stored offchain + uint256 orderId; + // Signature expiration timestamp + uint64 deadline; + } + + struct Signature { + // Signature expiration timestamp + uint64 deadline; + // Signature bytes (r, s, v) + bytes signature; + } + + struct FeeQuote { + // Unique ID and hash of order data used to validate order details stored offchain + uint256 orderId; + // Requester of order + address requester; + // Fee amount in payment token + uint256 fee; + // Timestamp of fee quote + uint64 timestamp; + // Signature expiration timestamp + uint64 deadline; + } + + struct PricePoint { + // Price specified with 18 decimals + uint256 price; + uint64 blocktime; + } + + /// @dev Emitted order details and order ID for each order + event OrderCreated(uint256 indexed id, address indexed requester, Order order, uint256 feesEscrowed); + /// @dev Emitted for each fill + event OrderFill( + uint256 indexed id, + address indexed paymentToken, + address indexed assetToken, + address requester, + uint256 assetAmount, + uint256 paymentAmount, + uint256 feesTaken, + bool sell + ); + /// @dev Emitted when order is completely filled, terminal + event OrderFulfilled(uint256 indexed id, address indexed requester); + /// @dev Emitted when order cancellation is requested + event CancelRequested(uint256 indexed id, address indexed requester); + /// @dev Emitted when order is cancelled, terminal + event OrderCancelled(uint256 indexed id, address indexed requester, string reason); + + /// ------------------ Getters ------------------ /// + + /// @notice Hash order data for validation and create unique order ID + /// @param order Order data + /// @dev EIP-712 typed data hash of order + function hashOrder( + Order calldata order + ) external pure returns (uint256); + + /// @notice Status of a given order + /// @param id Order ID + function getOrderStatus( + uint256 id + ) external view returns (OrderStatus); + + /// @notice Get remaining order quantity to fill + /// @param id Order ID + function getUnfilledAmount( + uint256 id + ) external view returns (uint256); + + /// @notice Get received amount for an order + /// @param id Order ID + function getReceivedAmount( + uint256 id + ) external view returns (uint256); + + /// @notice Get fees in payment token escrowed for a buy order + /// @param id Order ID + function getFeesEscrowed( + uint256 id + ) external view returns (uint256); + + /// @notice Get cumulative payment token fees taken for an order + /// @param id Order ID + /// @dev Only valid for ACTIVE orders + function getFeesTaken( + uint256 id + ) external view returns (uint256); + + /// @notice Reduces the precision allowed for the asset token quantity of an order + /// @param token The address of the token + function orderDecimalReduction( + address token + ) external view returns (uint8); + + /// @notice Get worst case fees for an order + /// @param sell Sell order + /// @param paymentToken Payment token for order + /// @return flatFee Flat fee for order + /// @return percentageFeeRate Percentage fee rate for order + function getStandardFees(bool sell, address paymentToken) external view returns (uint256, uint24); + + /// @notice Get total standard fees for an order + /// @param sell Sell order + /// @param paymentToken Payment token for order + /// @param paymentTokenQuantity Payment token quantity for order + function totalStandardFee( + bool sell, + address paymentToken, + uint256 paymentTokenQuantity + ) external view returns (uint256); + + /// @notice Check if an account is locked from transferring tokens + /// @param token Token to check + /// @param account Account to check + /// @dev Only used for payment tokens + function isTransferLocked(address token, address account) external view returns (bool); + + /// @notice Get the latest fill price for a token pair + /// @param assetToken Asset token + /// @param paymentToken Payment token + /// @dev price specified with 18 decimals + function latestFillPrice(address assetToken, address paymentToken) external view returns (PricePoint memory); + + /// ------------------ Actions ------------------ /// + + /// @notice Lock tokens and initialize signed order + /// @param order Order request to initialize + /// @param orderSignature Signature and deadline for order + /// @param feeQuote Fee quote for order + /// @param feeQuoteSignature Signature for fee quote + /// @return id Order id + /// @dev Only callable by operator + function createOrderWithSignature( + Order calldata order, + Signature calldata orderSignature, + FeeQuote calldata feeQuote, + bytes calldata feeQuoteSignature + ) external returns (uint256); + + /// @notice Request an order + /// @param order Order request to submit + /// @param feeQuote Fee quote for order + /// @param feeQuoteSignature Signature for fee quote + /// @return id Order id + /// @dev Emits OrderCreated event to be sent to fulfillment service (operator) + function createOrder( + Order calldata order, + FeeQuote calldata feeQuote, + bytes calldata feeQuoteSignature + ) external returns (uint256); + + /// @notice Request an order with standard fees + /// @param order Order request to submit + /// @return id Order id + /// @dev Emits OrderCreated event to be sent to fulfillment service (operator) + function createOrderStandardFees( + Order calldata order + ) external returns (uint256); + + /// @notice Fill an order + /// @param order Order request to fill + /// @param fillAmount Amount of order token to fill + /// @param receivedAmount Amount of received token + /// @dev Only callable by operator + function fillOrder(Order calldata order, uint256 fillAmount, uint256 receivedAmount, uint256 fees) external; + + /// @notice Request to cancel an order + /// @param id Order id + /// @dev Only callable by initial order requester + /// @dev Emits CancelRequested event to be sent to fulfillment service (operator) + function requestCancel( + uint256 id + ) external; + + /// @notice Cancel an order + /// @param order Order request to cancel + /// @param reason Reason for cancellation + /// @dev Only callable by operator + function cancelOrder(Order calldata order, string calldata reason) external; + +}