owned this note
owned this note
Published
Linked with GitHub
---
tags: spec, treasury, swap, stonks, dao-ops
---
# Lido Stonks. Specification
## Problem statement
Lido DAO faces a major challenge in optimizing treasury operations, especially in the areas of token usage, market adaptation and crisis response, as exemplified by cases such as the depeg of DAI and USDC. To solve this problem, it is necessary to create a specialized [treasury management committee](https://research.lido.fi/t/proposal-to-approve-lido-dao-treasury-management-principles-and-authorize-the-formation-of-a-treasury-management-committee/4279) (TMC) and provide it with a technical solution that should ensure efficient and secure management of assets by the committee, while limiting direct access to funds.
### General requirements
* proposed solution should be able to be used by both TMC and Lido DAO;
* funds should only be available for a limited set of transactions, pre-approved by Lido DAO;
* proposed solution should have limited access to the Lido DAO treasury.
### Technical requirements
* every trade should be MEV-protected;
* every trade should be "price-guaranteed" (the trade is expected to be canceled if the minimum exchange amount is not received);
* beneficiary of every trade should be Lido DAO (treasury);
* variety of tokens for trade (both input and output) should be strictly limited by DAO;
* TMC should has the ability to operate trades (move funds from treasury, place orders, move funds back to treasury in case of unsuccessful trade);
* TMC should never take custody of Aragon funds ([post on the research forum](https://research.lido.fi/t/proposal-to-approve-lido-dao-treasury-management-principles-and-authorize-the-formation-of-a-treasury-management-committee/4279)).
### Primary use-cases
* selling stETHs to stables (DAI/USDC/USDT)
* rebalancing the amounts of different stables (DAI/USDC/USDT)
## Proposed solution
Since there are already two ways to transfer funds from the treasury, it is proposed to develop a new technical solution `Stonks`, which will contain a fixed set of necessary operations for exchanging funds and managing the treasury.
### Components
1. [Easy Track](https://github.com/lidofinance/easy-track) (`TopUpAllowedRecipient` factory) or Aragon vote (in case of emergency) for token transfering. These contracts will be used as is.
2. [Stonks](https://github.com/lidofinance/stonks) - set of contracts is receiver of tokens from the first step and container of swap operations set by Lido DAO;
3. [Cow Protocol](https://cow.fi) - a fully permissionless trading protocol that enables batch auctions to maximize liquidity via Coincidence of Wants (CoWs) in addition to tapping all available on-chain liquidity;
4. [ChainLink](https://chain.link) - a decentralized oracle network that provides the USD price of Ethereum’s native cryptocurrency and tokens.
There are two directions of tokens movement between components:
1. Swap (happy path) - when tokens are transferred from the DAO Treasury to Stonks for swapping using CoW Protocol. Swap proceeds are sent directly to the DAO Treasury (CoW Protocol API allows it).
2. Recovery (contingency) - if the swap doesn’t execute for any reason, tokens must be returned to the DAO Treasury.
### Happy path
![image](https://hackmd.io/_uploads/SkfavhEhT.png)
**Step 0.** DAO Treasury committee creates (in a permissionless way) a new `Stonks` instance with specific params and proposes DAO to add that contract to the EasyTrack allowed recipients.
**Step 1.** Tokens are transfered from the DAO Treasury to the `Stonks` instance via Easy Track limited factories or Aragon vote via regular ERC-20 `transfer`.
**Step 2. Order placement.** Manager (TMC multisig) makes `Stonks` to deploy of a new `Order` contract via `placeOrder` function and it automatically sends all available assets there. After deploy `Order` creates allowance to the CoW vault relater contract and waits until this order is completed.
**Step 3. Order creation.** Easy Track UI checks events and allows manager to create offchain order on Cow Protocol. This UI is trustless and can be used by anyone.
**Step 4. The Swap.** At the moment of order fulfilment CoW Protocol debits funds from `Order` contract & sends all exchanged assets to the DAO Treasury.
## Recovery (contingency) flow
![image](https://hackmd.io/_uploads/SJLg_h43T.png)
If for any reason swap isn't going through, there must be method to recover the tokens. Tokens can be at `Stonks` (before the swap is requested) or at `Order` (once the swap is requested on `Stonks`).
**Step 4.** The swap hasn't happened and order is expired. Anyone can permissionlessly return all funds to the `Stonks` contract for further actions. From this moment `Order` contract becomes inactive.
**Step 5.** At the moment in case of market turbulency manager (or DAO) can decide what to do next:
* send all funds to the DAO Treasury back;
* create a new order again.
## Design
### Assumptions and limitations
Trades are conducted in the model `sell X -> buy S`, where:
- S is a USD/ETH/EUR-pegged coin.
- X is a token that is permitted to be transferred from the Treasury. Adjustments for permissions may be required in accordance with [LIP-13](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-13.md), and new factories for limited payments might need to be deployed as per [LIP-19](https://github.com/lidofinance/lido-improvement-proposals/blob/develop/LIPS/lip-19.md).
- X has an established price feed in the Chainlink registry for USD/ETH/EUR.
The following cases are not covered on the current implementation:
- `sell S -> buy LDO` (LDO isn't pegged to ETH/USD/EUR).
These clarifications help to better understand the limitations and capabilities of the proposed system, providing clarity on the types of transactions that are supported within the current design.
### General diagram
![image](https://hackmd.io/_uploads/r1ASBaEnT.png)
### Factory deployment approach
It was decided that deploying contracts from a contract factory is a more convenient solution, which makes it possible to flexibly set instance parameters, while being sure that the bytecode of the contracts is completely the same.
Given the need to set certain parameters for all contract instances, it is very convenient to use immutable variables in the factory contract: this way we guarantee that when deploying a new contract, critical parameters (for example, the treasury address) cannot be changed. All other parameters are set during instance deployment, which makes it possible to create multiple instances in a flexible way.
As a result, there are two factory contracts:
#### AmountConverterFactory
Stores an immutable variables:
- [`feedRegistry`](https://docs.chain.link/data-feeds/feed-registry) address
When deploying a new instance, you need to set converter parameters:
- `conversionTarget` ([explanation](#Price-checking))
- `allowedTokensToSell`
- `allowedTokensToBuy`
- `priceFeedsHeartbeatTimeouts`
**Note**: Lido DAO should accept the token list. This should happen at the moment when the recipient of tokens in EasyTrack `TopUpAllowedRecipients` factory is set to a specific instance of Stonks which works with specific `AmountConverter` instance.
#### StonksFactory
During factory deployment, a sample order is created, which will then be used to create a minimal proxy in Stonks.
Stores an immutable variables:
- `agent`
- `orderSample`
When deploying a new instance, you need to set [trade parameters](#Trade-parameters).
### Setting up trade parameters
In order to set the parameters of upcoming swaps, it is necessary to deploy new instance of the `Stonks` contract. After deploying an instance, all parameters are fixed and only the number of tokens for sale can change (depending on the balance on the instance).
**Note**: `Stonks` instance supports the only swap direction (for example `stETH` -> `DAI`) and immutable set of trade paramters. To use any other swap direction it is neccessary to deploy new `Stonks` instance with required parameters.
#### Trade parameters:
- `manager`
- `tokenFrom`
- `tokenTo`
- `amountConverter`
- `orderDurationInSeconds`
- `marginInBasisPoints`
- `priceToleranceInBasisPoints`
**Note**: Every `Stonks` requires to be added as a recipient of `TopUpAllowedRecipient` EasyTrack factory so in this case TMC can operate funds from Lido DAO treasury. `Stonks` also can be filled up with any amount token from any user. In this case TMC could recover this tokens to Lido DAO treasury or proceed the trade.
### Order placement
Each time a new order is going to be placed, the Stonks contract initiates the creation of a new `Order` contract instance. This is achieved through making a minimal proxy of the master template of the `Order` contract. The cloning process, powered by the OpenZeppelin Clones library, is a gas-efficient way to create a new contract instance for each trade. These clones are intended for single-use, ensuring that every transaction is processed with a new contract instance. This isolation is critical for maintaining the integrity of each trade, preventing any potential interference or overlap between different swaps. It also enhances the security of the system by ensuring that each contract instance handles only its designated trade.
During the order placement, the required amount (all tokens on balance) of the `tokenFrom` is transferred from the `Stonks` contract to the newly created `Order` contract. This step is crucial as it earmarks the funds for the specific trade, effectively locking them in the `Order` contract until the trade is executed or invalidated.
After the fund transfer, the new `Order` contract is initialized with specific [trade parameters](#Trade-parameters) received from `Stonks`. This initialization is essential for setting up the trade according to the strategy and market conditions at the time of order placement.
As a result `Order` contract sets allowance for CoW vault relayer and creates an event to be picked up for offchain UI/script and sent to the CoW Protocol offchain API ([details](#CoW-Protocol-onchain-order-creation)).
### Price checking
To ensure the security of the transaction concerning the amount of tokens to be received as a result of the exchange, it's needed to determine a minimum acceptable amount at the moment of the order creation to ensure that trade is corresponds to market conditions and maximum acceptable amount for order invalidation at the moment of order fullfilment in case market conditions has been changed.
![image](https://hackmd.io/_uploads/SyBVmm8T6.png)
It is decided to use ChainLink price feeds against `conversionTarget` [stETH/USD, USDC/USD, USDT/USD, DAI/USD] using a [chainling feed registry](https://docs.chain.link/data-feeds/feed-registry) for our purposes to avoid double convertation and increasing sensivity during token comparing (stETH/USD -> USD/DAI). So at this stage, we consciously decide to refuse transfers to another token and expect that the exchange rate of the token to the `conversionTarget` will always be equal or close to equal.
When an order is created, `Order` contract invokes the `estimateTradeOutput` function to determine the expected amount of `tokenTo`, based on the current balance of `tokenFrom`. Once the output amount is obtained, `estimateTradeOutput` calculates the margin amount using the `marginInBasisPoints` parameter and then deducts this margin from the trade's output amount. Since this moment `estimateTradeOutput` is set in `Order` as `buyAmount` contract until order fulfilment. This calculation is necessary in order to:
- cover the exchange fees of the CoW Protocol
- cover the possible chainlink price deviation (chainlink price feed has specific sensitivity thresholds, at which the price might not get updated)
![image](https://hackmd.io/_uploads/Bk4UFpE2p.png)
**Note**: Since we are using the CoW Protocol, setting the `buyAmount` value ensures that the Order is invalid if fewer amount is offered.
At the moment of order fulfillment it's necessary to account for the potential scenario of a sudden market condition changes. For example:
1. An order is created with a price of 1eth=1000usd.
2. The price suddenly rises so that 1eth=1100usd.
3. At this stage, the order should be invalidated because of a potential loss.
When the `Order` is ready for execution, the CoW Protocol relayer calls `isValidSignature` (since `Order` is using the `eip1271` signature) method on the contract to check the validity of the swap. At this stage it is very convenient to put a second price check and invalidate the order in case the current transaction conditions are no longer satisfactory. At this moment `Order` contract invokes the `estimateTradeOutput` function again to determine the expected amount of `tokenTo` for this exact moment. If price spike happend, `Order` contract computes `priceToleranceAmount` (computed using the `priceToleranceInBasisPoints` parameter) from `buyAmount` set on the previous step to tolerate new market condition. If difference between newly received `currentCalculatedBuyAmount` and `buyAmount` is bigger than `priceToleranceAmount` order becomes invalidated.
![image](https://hackmd.io/_uploads/H1Qaq6N26.png)
### Token recovery
It's necessary to anticipate scenarios in which tokens are accidentally transferred to the `Stonks` or `Order` contract due to unforeseen circumstances. For this purpose, it's needed to implement `AssetRecoverer` contract with `recoverERC20`, `recoverEther`, `recoverERC721`, `recoverERC1155` methods, which will allow tokens to be withdrawn from the balance and sent to the Treasury (it's essential that the Treasury is the final recipient of any token). Only the manager or DAO can execute these methods, preventing accidental triggers.
It's important to remember that tokens awaiting exchange might be stored on the `Order` contract. To ensure the security of the transaction, the execution of `recoverERC20` shouldn't be allowed for tokens involved in an ongoing transaction until the transaction is deemed invalid by time.
Both contracts `Stonks` and `Order` are inherited from `AssetRecoverer` contract and eligible to recover tokens.
Moreover, it's necessary to allow to recover tokens that were going to be traded, but trade hasn't happend by any reason. For this purpose `Order` contract should has `recoverTokenFrom` method that allows to move `tokenFrom` tokens back to `Stonks` contract to be decided what to do next.
**Note**: The only constraint for this method is: `tokenFrom` cannot be recovered until `Order` is invalidated.
## CoW Protocol onchain order creation
To generate an order in Cow Protocol, it's necessary to create offchain data, execute an API request, passing that data as a parameter. The response to this request will be the UID of the offchain order.
Since the creation of an offchain order doesn't require permission, it's essential to ensure its non-execution if any parameters are altered. For this purpose, we will use an onchain order and its hash value. This hash will be saved in the order contract and will be checked every time CoW Protocol attempts to execute the order. If a malicious actor creates an offchain order with parameters different from those fixed in the contract, the hash values will not match, and the order will not be executed.
Each onchain order must comply with the [GPv2Order](https://github.com/cowprotocol/contracts/blob/main/src/contracts/libraries/GPv2Order.sol#L11) interface, which includes:
- `sellToken`
- `buyToken`
- `sellAmount`
- `buyAmount`
- `validTo`
- `feeAmount`
- `receiver`
- `kind`
- `partiallyFillable`
as well as several technical meta parameters:
- `appData`
- `sellTokenBalance`
- `buyTokenBalance`
This struct is going to be hashed by [EIP-712](https://eips.ethereum.org/EIPS/eip-712) using [GPv2Order.hash](https://github.com/cowprotocol/contracts/blob/main/src/contracts/libraries/GPv2Order.sol#L133) method of CoW Protocol contract.
For signing this type of transaction [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271) should be used. Cow Protocol [allows](https://docs.cow.fi/tutorials/how-to-place-erc-1271-smart-contract-orders) to create smart contract orders with EIP-1271 signatures for offchain trades so trade parameters should be checked in the `isValidSignature` method of the `Order` contract as well.
### Token to buy/sell
These parameters are set in immutable variables of the Stonks contract. It indicates the tokens are going to be exchanged. Each Stonks contract is fixed to work with only two tokens and only in one direction. To exchange other tokens, a different contract must be used.
### Amount to buy/sell and kind of order
To limit the possibility of non-optimal use of funds to create a new order, it was decided to use the entire amount on the balance of the `Stonks` contract. Consequently, the `sellAmount` parameter emerges from executing `balanceOf(this)` on the `Stonks` contract and is can't be changed by committee.
The `KIND` parameter plays a pivotal role in the transaction calculation math:
When set to `SELL`, it signals to CowSwap the intention to sell a fixed quantity of N tokens. As a result, the `buyAmount` parameter becomes variable during the exchange.
Conversely, when `BUY` is selected, it indicates a commitment to purchase a fixed number of N tokens. This setting causes the `sellAmount` parameter to fluctuate throughout the transaction.
The decision is made to utilize `KIND=sell`.
### Partially fillable
This parameter dictates the feasibility of partially fulfilling an order over a specified time frame. When set to `true`, it allows, for example, an exchange order of 100 ETH to be executed in 10 batches. This approach can be advantageous as it may lead to a more favorable price. However, there is a drawback: the order might only be partially completed before exceeding its validity date. Subsequently, the unfulfilled portion would need to be returned to the Stonks contract, necessitating a restart of the process.
Conversely, setting the parameter to `false` ensures that the order is either fully executed or not executed at all. In this context, the decision is made to set the parameter to `false`.
### Fee amount
The choice to utilize limit orders is made due to the impact of trade size on price formation when using regular market orders. The aim is to establish a minimum acceptable amount to receive for the order, hence the preference for limit orders.
When opting to place a limit order, it is mandatory to set the fee amount to 0, as per the guidelines for limit order fees.
Key Features
- Price Specification: Users determine a specific price level for buying or selling tokens.
- Quantity: The user specifies the number of tokens to be traded.
- Expiration: Limit orders can be set with a specific duration, known as time-to-live (TTL), after which unfilled orders are automatically cancelled.
- Non-Guaranteed Execution: Execution of a limit order is not assured if the market price never meets the order's specified level.
## CoW Protocol offchain order creation
To make a token swap, it's necessary to notify CoW Protocol that a specific order needs to be added to the order book. This requires transmitting the parameters for creating the order to the CoW API.
After the order creation onchain, a bot (bot/frontend/etc.) should be triggered, which will take data from the event and form the data for the CoW API.
It's important to remember that for validating the order, the hash of the onchain transaction parameters is used, which prevents any alteration of the final transaction amount or other parameters.
## Technical specification
### Contract `Ownable`
<details>
The `Ownable` contract designed to provide a basic access control mechanism. It allows for two distinct roles: an agent and a manager. The agent is defined at the time of contract deployment and is immutable, while the manager can be appointed and potentially changed by the agent.
#### `constructor`
Initializes the contract by setting the agent.
```solidity!
constructor(address agent_)
```
#### Constraints:
- Ensures the agent is not the zero address.
#### `setManager`
Allows the agent to set or change the manager.
```solidity!
function setManager(address manager_) external onlyAgent
```
#### `onlyAgent`
Restricts function access exclusively to the agent.
```solidity!
modifier onlyAgent()
```
#### `onlyAgentOrManager`
Restricts function access to either the agent or the manager.
```solidity!
modifier onlyAgentOrManager()
```
</details>
### Contract `AssetRecoverer` (inherits `Ownable`)
<details>
The AssetRecoverer contract implements a mechanism to recover ERC20, ERC721, ERC1155 tokens and Ethers that have been mistakenly sent to a contract. This is useful in scenarios where a user has accidentally transferred tokens to the contract's address, providing a means to return the tokens to their owner. Such a contract typically includes functions to check balances and transfer tokens back to the owner or an authorized agent.
#### `recoverERC20`
Allows the agent or manager to recover ERC20 tokens held by the contract.
```solidity!
function recoverERC20(address tokenAddress, uint256 amount) public onlyAgentOrManager
```
#### Constraints:
- The function should ensure that the contract has a sufficient amount of tokens to recover before calling transfer.
- There should be a check for the successful execution of the transfer function.
- Can send tokens only on Agent address.
#### `recoverERC721`
Allows the agent or manager to recover ERC721 tokens held by the contract.
```solidity!
function recoverERC721(address token, uint256 tokenId) external onlyAgentOrManager
```
#### Constraints:
- There should be a check for the successful execution of the transfer function.
- Can send tokens only on Agent address.
#### `recoverERC1155`
Allows the agent or manager to recover ERC721 tokens held by the contract.
```solidity!
function recoverERC1155(address token, uint256 tokenId) external onlyAgentOrManager
```
#### Constraints:
- There should be a check for the successful execution of the transfer function.
- Can send tokens only on Agent address.
#### `recoverEther`
Allows the agent or manager to recover Ether tokens held by the contract.
```solidity!
function recoverEther() external onlyAgentOrManager
```
#### Constraints:
- There should be a check for the successful execution of the transfer function.
- Can send tokens only on Agent address.
</details>
### Contract `Stonks` (inherits `AssetRecoverer`)
<details>
The `Stonks` contract serves as a trading facilitator between two specified ERC20 tokens. It enables the creation and management of orders through an associated `Order` contract instance. The contract is designed to interact with an external `AmountConverter` (via Chainlink price feed) and after initialization funds become withdrawable for an offchain settlement (CoW Protocol). It inherits from the `AssetRecoverer` contract to ensure safe recovery of ERC20, etc. tokens.
#### `constructor`
Initializes the Stonks contract with key trading parameters. Stores essential parameters for trade execution in immutable variables, ensuring consistency and security of trades.
```solidity!
constructor(
address agent_,
address manager_,
address tokenFrom_,
address tokenTo_,
address amountConverter_,
address orderSample_,
uint256 orderDurationInSeconds_,
uint256 marginInBasisPoints_,
uint256 priceToleranceInBasisPoints_
) AssetRecoverer(agent_)
```
#### Constraints:
- None of the address parameters can be the zero address.
- The `tokenFrom_` and `tokenTo_` addresses must not be the same.
- `marginInBasisPoints_` and `priceToleranceInBasisPoints_` can't be more than `BASIS_POINTS_PARAMETERS_LIMIT` = 1000.
- `orderDurationInSeconds_` can't be less than `60 seconds` and more than `24 hours`.
#### `placeOrder`
Initiates a new trading order by creating an Order contract clone. Transfers the `tokenFrom` balance to the Order clone and initializes it with the `Stonks` manager settings for execution. Sets the minimal acceptable result amount of the trade.
```solidity!
function placeOrder(uint256 minBuyAmount) external onlyAgentOrManager
```
#### Constraints:
- There must be a balance of the `tokenFrom` in the Stonks contract before placing an order.
- To avoid order spamming with rebasable tokens (stETH for example), order creation should be restricted with minimal amount (10 wei).
#### `estimateTradeOutput`
Estimates output amount for a given trade input amount. Uses `AmountConverter` for output estimation.
```solidity!
function estimateTradeOutput(uint256 amount) public view returns (uint256)
```
#### `estimateTradeOutputFromCurrentBalance`
Estimates trade output based on current input token balance. Uses `estimateTradeOutput` for output estimation.
```solidity!
function estimateOutputFromCurrentBalance() external view returns (uint256)
```
#### Constraints:
- Amount shouldn't be 0.
#### `getOrderParameters`
Returns trading parameters from Stonks for use in the Order contract.
```solidity!
function getOrderParameters() external view returns (address tokenFrom, address tokenTo, uint256 orderDurationInSeconds)
```
#### `getPriceTolerance`
Returns price tolerance parameter from Stonks for use in the Order contract.
```solidity!
function getPriceTolerance() external view returns (uint256 priceToleranceInBasisPoints)
```
</details>
### Contract `Order` (inherits `AssetRecoverer`)
<details>
Handles the execution of individual trading orders for the Stonks contract on CoW P.
#### `constructor`
This constructor sets up necessary parameters and state variables to enable the contract's interaction with the CoW Swap protocol. Used immutable variables.
```solidity!
constructor(address agent_, address settlement_, address relayer_) AssetRecoverer(agent_)
```
#### `initialize`
Initializes the contract for trading by defining order parameters and approving tokens. This function sets the buy amount (if buy amount received from chainlink based on market conditions is lower then minBuyAmount, sets minBuyAmount), sets the order parameters, and approves the token for trading.
```solidity!
function initialize(uint256 minBuyAmount_, address manager_) external
```
#### Constraints:
- Can only be executed once as it is intended for a single trade setup.
#### `isValidSignature`
Validates the order's signature and ensures compliance with price and timing constraints. Implements EIP-1271 for onchain signature.
#### Parameters:
- `bytes32 hash`: The hash of the order to verify.
- `bytes calldata signature`: The signature to validate (unused, as the verification is implicit).
#### Returns:
`bytes4`: Returns the magic value `ERC1271_MAGIC_VALUE` if the signature is valid.
#### Constraints:
- The provided hash must match the stored order hash.
- The current block timestamp must be less than or equal to validTo.
- The function should check for the current token prices to check that there are no price spikes on market.
#### `recoverTokenFrom`
Allows for the cancellation of the order and returns the tokens to the `Stonks` contract if the order has expired.
```solidity!
function recoverTokenFrom() external
```
#### Constraints:
- Can only be executed if the order has expired (validTo is less than the current block timestamp).
#### `recoverERC20`
Facilitates the recovery of ERC20 tokens from the contract, except for the token involved in the order.
```solidity!
function recoverERC20(address token_, uint256 amount) public override onlyAgentOrManager
```
#### Constraints:
- Cannot be used to recover the `tokenFrom`.
- Can only be executed by the manager or an authorized agent.
#### Event `OrderCreated`
Emitted when a new order is initialized.
#### Parameters:
- `address indexed order`: The address of the order contract.
- `bytes32 orderHash`: The hash of the order.
- `GPv2Order.Data orderData`: The data struct containing the order details.
</details>
### Contract `AmountConverter`
<details>
This contract provides functionalities to retrieve expected token conversion rates based on the Chainlink Price Feed.
The primary function `getExpectedOut` is the main point of interaction. It fetches the price of the provided token to `conversionTarget` from the Chainlink Price Feed and then calculates the expected amount of the output token based on the input amount of the sellToken.
#### `constructor`
The constructor initializes the contract with the Chainlink Feed Registry address and lists of tokens that are allowed to be sold and stable tokens that can be purchased.
```solidity!
constructor(
address feedRegistry_,
address conversionTarget_,
address[] memory allowedTokensToSell_,
address[] memory allowedTokensToBuy_,
uint256[] memory priceFeedsHeartbeatTimeouts_
)
```
#### Restrictions
- The `_feedRegistry` must not be the zero address.
- Each address in `_allowedTokensToSell` and `_allowedStableTokensToBuy` must not be the zero address.
- Each token in `_allowedTokensToSell` must have an associated price feed in the Chainlink Feed Registry.
- `priceFeedsHeartbeatTimeouts_` should be in sync by index with `allowedTokensToSell_`
#### `getExpectedOut`
Calculates the expected amount of `tokenTo_` that one would receive for a given amount of `tokenFrom_`.
This function computes the expected output amount of `tokenTo_` when selling `tokenFrom_`. It uses the Chainlink Price Feed to get the current price of `tokenFrom_` in terms of the `conversionTarget` (usually USD). The function then adjusts this price based on the token decimals and returns the expected amount of `tokenTo_` one would receive for the specified `amountFrom_` of `tokenFrom_`. This function assumes that `tokenTo_` is equivalent in value to the `conversionTarget`.
```solidity
function getExpectedOut(address tokenFrom_, address tokenTo_, uint256 amountFrom_) external view returns (uint256 expectedOutputAmount)
```
#### Returns
- `expectedOutputAmount`: The calculated amount of `_tokenTo` tokens expected to be received.
#### Restrictions
- `_tokenFrom` and `_tokenTo` must not be the same address.
- `_tokenFrom` must be an allowed token to sell.
- `_tokenTo` must be an allowed stable token to buy.
</details>