-
Notifications
You must be signed in to change notification settings - Fork 96
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
Single flashloan encoding #3279
Conversation
serde_json::from_str(&response.text().await?) | ||
.context("invalid app data document")?; | ||
match appdata.full_app_data == "{}" { | ||
true => None, // empty app data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think to avoid fetching the underlying app data for 0x000...000
(i.e. "{}") over an over again we need to store a dummy value for that hash.
@@ -173,6 +173,51 @@ pub fn tx( | |||
min: None, | |||
prices: auction.prices().clone(), | |||
}; | |||
|
|||
// Add all interactions needed to move flash loaned tokens around |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably makes sense to move this and the other flashloan specific encoding logic into a separate function. Also technically all of this should happen at the very end to make sure no other interactions get added before or after the flashloan interactions.
crates/e2e/tests/e2e/flashloans.rs
Outdated
// Receiver is flashloan wrapper, so that borrowed funds can be returned to the lender | ||
receiver: Some(onchain.contracts().flashloan_wrapper.address()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not how we probably end up using this feature. However, while thinking about how to correct this code I realized that we didn't think some of the details through enough. Fine for now to show that the wrapper contract works but ultimately we should probably have an e2e test that repays a debt with collateral since that is the primary use case for this feature AFAIK.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a discussion we had that led to this implementation:
cowprotocol/flash-loan-wrapper-solver#2 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ended up having a discussion with Felix and Federico about the exact details for this. The decision was made that the underlying order should always have the settlement
contract as the receiver
. That allows the order to be oblivious of the flashloan helper contract that actually ends up getting used. The driver encoding would then have to take this into consideration and transfer the respective funds from the settlement contract to the flashloan helper contract itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks.
Will adjust the code and tests.
.transfer_from( | ||
contracts.settlement().address().into(), | ||
flashloan_wrapper.address(), | ||
flashloan.amount.0, // or order buy amount? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our initial implementation assumes the values are always the same. But if they were not, using the flashloan amount would lead to better outcomes:
if flashloan.amount > order.buyAmount
the solution would not simulate in the first place which encourages solvers to make sure the buy amount covers the flashloan amount. Unless the settlement contract has enough fees already buffered but then this would still count as negative slippage which is a punishment for the solver.
if flashloan.amount < order.buyAmount
the difference would stay in the settlement contract instead of accumulate in the wrapper contract which is more convenient for us and would count as positive slippage the solver can claim when paying out rewards.
crates/e2e/tests/e2e/flashloans.rs
Outdated
} | ||
}; | ||
|
||
// pay 9 bps of flashloan fee to the AAVE pool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where does this number come from?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I googled it. After double checking it turned out I got the wrong number. Updated it to be 5 bps. Still didn't link a reference in the source code in the driver encoding since this hopefully going to change before the release anyway.
crates/e2e/tests/e2e/flashloans.rs
Outdated
// Receiver is always the settlement contract, so driver will have to manually send funds to | ||
// solver wrapper (flashloan borrower) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this happen in this test? is baseline doing this? or is it a TODO or not needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reworded the comment
&order.data().hash_struct(), | ||
))); | ||
|
||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please can you add comments to this section? 🙏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of a comment I have a log explaining what is happening. Can move it above the block though to avoid confusion.
crates/e2e/tests/e2e/flashloans.rs
Outdated
let current_safe_nonce = trader.nonce().await; | ||
|
||
// Build appdata that does: | ||
// 1. take out a 1 WETH flashloan for the trader |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- is not actually part of the appdata right, it's already done above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you confuse flashloan with borrowing the WETH (which indeed happens above)?
The flashloan is not explicitly encoded in an interaction but it's still part of the appdata as the flashloan
hint. Updated the comment to make this clearer.
crates/driver/src/domain/eth/mod.rs
Outdated
#[derive(Debug, Clone)] | ||
pub struct Flashloan { | ||
pub lender: ContractAddress, | ||
pub borrower: ContractAddress, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The borrower
shouldn't be an ordinary Address
? Is there a requirement that it is a contract?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. Indeed any Address could be the receiver of a flashloan.
solution | ||
.flashloans | ||
.into_iter() | ||
.map(|flashloan| eth::Flashloan { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this conversion be moved into From
trait implementation in the dto Flashloan
?
This function is quite long with lots of indentation, and moving all those conversions to implementation blocks will improve the code readability a bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From
implementations turned out to be a double edged sword with the domain driven design that requires a ton of conversions because if you use an IDE to jump to the definition of the .into()
call it will jump to the blanket implementation on the From
trait which is not really helpful.
.unwrap(); | ||
|
||
let trader_usdc = balance(&web3, trader.address(), usdc.address()).await; | ||
assert!(trader_usdc > to_wei_with_exp(47_000, 6)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the trader is not getting back the whole collateral value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Double that. From the logic above, I couldn't understand where 3000 USDC remain. A comment would be helpful. Ah, it is related to 1 weth debt. Got it. Still, a comment would be helpful.
assert!(settlement_weth < 100_000_000u128.into()); | ||
tracing::info!("settlement contract only has dust amounts of WETH"); | ||
|
||
assert!(balance(&web3, trader.address(), ausdc).await < 10_000.into()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the assertion above checks that the trader spent less than 3000 USDC on the loan, why do we have 10k aUSDC here? Should it be 3000 also?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aUSDC
only tracks how much USDC
you have currently deposited in AAVE (+ interest). Since the trader actually had to sell some USDC
they wipe out their entire debt (close to 0 aUSDC
) but don't get their entire USDC
back (~3000 sold for 1 WETH
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, but still not sure why 10k is used here. Should it be 3k due to the assertion above in the code?
assert!(trader_usdc > to_wei_with_exp(47_000, 6));
If this passes, and initially the trader had only 50k, then <3k of aUSDC remains, right? Or did I miss something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you overlooked the different orders of magnitude. The user has ~3000 actual USDC less (i.e. 3_000_000_000
atoms). The remaining debt is only 10_000
atoms which is not even 1ct of debt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aaah, i knew I missed something, thank you!
Description
Tackles #3218
Implements on flavor of the "repay debt with collateral" idea an a e2e test. The trader is a gnosis safe to make it easier to do all the necessary pre-interactions. The basic flow is like this:
flashloan
hint)This flavor of "repay debt with collateral" leaves trace amounts of debt in the AAVE pool. The alternative would be either over paying slightly or a bunch of very complex accounting logic using another helper contract. Both are more involved and not necessary to proof the concept.
Changes
some bug fixes / workarounds that were necessary to make it work:
Things not addressed by this PR
buyAmount
needs to beappdata.flashloan.amount + fee
. Ideally the driver would reduce the flashloan amount such thatloanedAmount + fee = totalSignedAmount
.How to test
added new forked e2e test
adjusted unit test for solvable orders filtering exception for flashloan orders