Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add flash loans #7

Closed
mootz12 opened this issue Dec 3, 2024 · 7 comments · Fixed by #30
Closed

Add flash loans #7

mootz12 opened this issue Dec 3, 2024 · 7 comments · Fixed by #30
Labels
enhancement New feature or request pool This issue impacts the pool

Comments

@mootz12
Copy link
Contributor

mootz12 commented Dec 3, 2024

Add flash loan capabilities to Blend.

Technical Notes

Add a new action type to Blend called "FlashBorrow"
-> FlashBorrow will immediately send the sepecified amount of tokens, then invoke "processFlashLoan" on the user. This will fail if the user is not a contract.

The usage of flash loans would be as follows:

  1. Deploy a flash loan strategy. (TODO - determine if interface is good)

    • Consider -> USDCXLMArbContract.processFlashLoan(from: Address, asset: Address, amount: i128) that takes in USDC to do some USDC<->XLM arbs and ends up with USDC.

    • The contract would need to return the USDC to from, or panic if some condition is not met

  2. Invoke the the Blend pool with the following request stack, such that from is the user conducting the flash loan (IE - the one that actually takes on the liabilities)

Requests:
-> SupplyCollateral 50,000 XLM (user -> from)
-> FlashBorrow 100,000 USDC (user -> contract)
-> Repay 95,000 USDC (user -> from)

After the requests, the health factor for from is checked. Assuming 50k XLM is enough collateral to cover a 5k USDC loan, the transaction would complete successfully.

One benefit here is that flash loan strategies can be deployed and used by anyone.

@mootz12 mootz12 added enhancement New feature or request pool This issue impacts the pool labels Dec 3, 2024
@heytdep
Copy link
Contributor

heytdep commented Dec 9, 2024

took a stab at adding this. This is still untested since we want to try using the snapshots. Best way for that might actually be a testing crate that generates various snapshots depending on the state required but that's out of scope for this issue.

The tldr is that:

  • A new FlashBorrow was added.
  • A new flash_borrow field was added to Actions.
  • FlashBorrow is almost equivalent to Borrow, the only difference is that instead of adding a pool transfer for the borrow, it's adding it to flash_borrow, which is handled before all the other actions, allowing the flash loan executor to execute the ideally profit-earning strategy before the transfers to unwind the hf (either supply collateral or repay) are carried.
  • the currently used interface is the xycloans standard inspired by erc3156 (https://github.com/xycloo/xycloans/tree/main/moderc3156)

I like this approach vs executing the transfer + invocation in the FlashBorrow action code block because it's more similar to the current design of the blend codebase and it's likely better for deriving the flash loan contract without changing the structure of requests.

The real main question here is how to handle the receiver contract, i.e the contract called by blend when executing the flash loan. Right now, it's from but this doesn't allow for receiver contracts to be cross-user (which will also probs require a change in the current receiver interface) and requires the receiver contract to be the one holding the actual positions. I wouldn't want to add a new parameter to the submission but neither spender or to seem better than from, maybe spender?

@mootz12
Copy link
Contributor Author

mootz12 commented Dec 9, 2024

Awesome!

I agree that from would not work as it is expected that flash loans contract will exist that should not hold positions in a pool.

For now, I would use to, given to is the destination of the tokens being sent.

It might be worth it to implement #11 alongside this and/or before extensive testing is written. This way, the FlashBorrow action will have a user (or something similar) and asset parameter that can specify both the asset being flash loaned and the flash loan contract.

@mootz12
Copy link
Contributor Author

mootz12 commented Dec 30, 2024

After further discussion, it was deemed that #11 will not be implemented, which means it does not make sense to implement flash borrow as a request, given we will not have the ability to pass a contract ID into the request type.

For now, it appears to make the most sense to do the following interface:

pub struct FlashBorrow {
    pub contract: Address,
    pub asset: Address,
    pub amount: i128
}

fn flash_borrow(
        e: Env,
        from: Address,
        flashBorrow: FlashBorrow
        requests: Vec<Request>,
    ) -> Positions;

in this case, it is assumed that spender to and from are the same account, as they are the conductors of the flash borrow, and to prevent ambiguity on which account will be receiving the funds from the flash borrow contract.

I am also OK leaving spender and to, if @heytdep or @markuspluna feel strongly about it.

@brozorec
Copy link
Contributor

Hey @mootz12 I understand the reasoning about adding the new fn flash_borrow() so that the flash loans logic doesn't mess around with the regular flows.

However, there are some elements that obscure my comprehension. Let me list the cases that come to my mind and the questions that pop up:

Vanilla flash loan

  1. User calls the pool to initiate a flash loan by providing a receiver contract that implements erc3156.
  2. The pool transfers amount to receiver and calls receiver.on_flash_loan(...)
  3. The receiver does whatever it wants with the borrowed amount and creates allowance (amount + some fee) for the pool.
  4. The pool calls token.transfer_from to retrieve the borrowed amount + fee from the receiver.

If we want this outcome where flash_borrow() is the entrypoint for triggering a flashloan, I don't get the purpose of requests. (This could be potentially some data passed to the receiver that will be used for smth within receiver or to call back the pool. The latter is impossible because Soroban doesn't support re-entrancy + there could be significant risks for the pool as well. Meaning we can discard this option.)

Flash loan as part of a bundle

Useful for collateral and debt swaps or liquidations, mostly in conjunction with a swapper.

Collateral Swap

  1. User has an open position with col as collateral and some debt in debt. They want to change the collateral for new_col.
  2. User constructs the following bundle of actions: Flashloan debt -> Repay debt -> Withdraw col -> Swap col for new_col -> Deposit new_col -> Borrow debt (+ some fee) -> Repay flash loan with debt

FIll Liquidation Auction

  1. There's an active liquidation auction with col and debt. Liquidator wants to fill the auction at a given moment.
  2. Liquidator constructs the following bundle of actions: Flashloan debt -> Repay debt -> Withdraw col -> Swap col for debt -> Repay flash loan with debt

We can see in those cases that the flash loan is the first action. Does it mean you suggest we handle first the flash loan in fn flash_borrow() and then, we build up the other actions from requests?

Do I understand properly the context? Can you please rectify my comprehension if necessary and provide more specs and details about the desired outcomes.
Thank you!

@mootz12
Copy link
Contributor Author

mootz12 commented Dec 31, 2024

Hey @brozorec!

The implementation will be slightly different than erc3156 flash loans in that, specifically, there is no expectation that the loan is repaid in full, rather that that account taking on the loan enough collateral to maintain the loan.

Thus, the requests are used to process the repayment of the loan (or, whatever a user might want to do post a flash loan).

Vanilla Flash loan

It would functionally work the same, but here is a concrete example of the implementation with Blend, lets say a user wants to take a flash loan out for 10k USDC to fill some arb opportunity, and the user wants to repay the full amount after.

  1. Pool creates a 10k USDC liability for user
  2. Pool transfers 10k USDC to contract
  3. Pool invokes contract.on_flash_loan (or exec_op as done in Xycloans already in Soroban)
  4. Pool processes the repay request
  5. Pool transfer_from 10k + fee USDC from user` to self
  6. Pool validates health factor for user (important if any liabilities remain)

Flash loan as part of a bundle

Collateral Swap

I'm not sure this would actually work in the existing implementation, at least easily in an atomic transaction, given no re-entrancy.

IIRC something like this would not be possible on Aave in an atomic transaction either? Assuming the flash loan new_debt cannot result in new_debt + existing_debt, the withdraw step would fail.

Fill Liquidation Auction

For a more concrete example, lets imagine there is USDC col and XLM liability liquidation, and liquidator operates in USDC.

  1. FlashBorrow USDC, contract swaps USDC for XLM
  2. request 1 -> repay XLM
  3. request 2 -> withdraw additional USDC collateral
  4. request 3 -> repay flash loaned USDC

The big win here actually comes from #4, since with transfer_from we can simplify the token transfers between 3-4, so as long as you got more collateral in 3 than you repay in 4 (which should be a given since you would wait to fill the auction otherwise to make money), you will receive USDC from the pool.

Let me know if that makes sense!

Could you share some more info about the collateral swap use case?

@brozorec
Copy link
Contributor

brozorec commented Jan 2, 2025

Noted, thank you @mootz12 I'll start working on that and will come back if more questions pop up.

Could you share some more info about the collateral swap use case?

As an example, the collateral swap is useful when the user has an open debt position with BTC as collateral and USDC as debt. They don't dispose the borrowed amount at the given moment (because they deployed it in high-yield farm). The user deems that XLM has a higher upward potential now and they want to swap their pledged BTC for XLM w/o closing the position.

The logic for a debt swap will be similar but this time, the reason to swap the borrowed asset (say USDC) for another one (say USDT) could be that the interest rate of USDC is now much higher than USDT and the user wishes to optimize their interest cost.


Btw there's another case for flash loans that might be applicable to Blend. It's building up and unwinding levered positions. For example, a user wants to leverage XLM; assuming 1 XLM = $0.5 and a target loan-to-value of 65%, they can achieve this by:

  • deposit 2000 XLM and borrow 650 USDC
  • swap the borrowed 650 USDC for 1300 XLM
  • deposit back 1300 XLM and borrow again 422 USDC (650 * 65%)
  • swap the borrowed 422 USDC for 844 XLM
  • deposit back 844 XLM and borrow again 274 USDC (422 * 65%)
  • swap the borrowed 274 USDC for 548 XLM and deposit

Their final position is → Supply: 4690 and Borrow: 1345 USDC. The user got ~2x leverage and they can keep looping but it's way more practical to accomplish this with a flash loan:

  • take a flash loan for 2690 XLM and deposit 4690 XLM (user's 2000 + 2690 flash-loaned)
  • borrow 1345 USDC and swap for 2690 XLM and repay the flashloan

Using a flash loan to unwind a levered position is even more practical.

Collateral Swap
I'm not sure this would actually work in the existing implementation, at least easily in an atomic transaction, given no re-entrancy.
IIRC something like this would not be possible on Aave in an atomic transaction either?

Yes, you're right it's difficult to make it work with the current implementation. Though, it is possible on Aave with the help o periphery contracts and the possibility to act on-behalf. Check here for more details.

@mootz12
Copy link
Contributor Author

mootz12 commented Jan 2, 2025

Yep, you can also enter and exit a levered position in a single transaction with FlashBorrow, and exiting is extremely useful.

Hopefully, as flash loan contracts get written, common implementations (swap USDC for XLM, or vice versa) can be shared by users, allowing easy/safe methods to do this.

Interestingly, with transfer_from and #4, users can enter and exit same asset levered positions without even executing a flash loan.


And just to clarify @brozorec, you are going to attempt to tackle #4? If you want to reach me quicker, you should join the Blend discord, and DM me there.

@mootz12 mootz12 linked a pull request Jan 15, 2025 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request pool This issue impacts the pool
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants