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 lending functionality #10

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,7 @@ version = "1.2.0"

[workspace.dependencies.sep-41-token]
version = "1.2.0"

[workspace.dependencies.moderc3156]
git = "https://github.com/xycloo/xycloans"
rev = "d9a7ae1"
2 changes: 1 addition & 1 deletion pool/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ soroban-fixed-point-math = { workspace = true }
cast = { workspace = true }
sep-40-oracle = { workspace = true }
sep-41-token = { workspace = true}

moderc3156 = { workspace = true}

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
Expand Down
83 changes: 76 additions & 7 deletions pool/src/pool/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub enum RequestType {
FillBadDebtAuction = 7,
FillInterestAuction = 8,
DeleteLiquidationAuction = 9,
FlashBorrow = 10,
}

impl RequestType {
Expand All @@ -48,6 +49,7 @@ impl RequestType {
7 => RequestType::FillBadDebtAuction,
8 => RequestType::FillInterestAuction,
9 => RequestType::DeleteLiquidationAuction,
10 => RequestType::FlashBorrow,
_ => panic_with_error!(e, PoolError::BadRequest),
}
}
Expand All @@ -57,6 +59,7 @@ impl RequestType {
pub struct Actions {
pub spender_transfer: Map<Address, i128>,
pub pool_transfer: Map<Address, i128>,
pub flash_borrow: Vec<(Address, i128)>, // we expect flash borrows not to be dynamic.
}

impl Actions {
Expand All @@ -65,6 +68,7 @@ impl Actions {
Actions {
spender_transfer: Map::new(e),
pool_transfer: Map::new(e),
flash_borrow: Vec::new(e),
}
}

Expand All @@ -83,6 +87,10 @@ impl Actions {
amount + self.pool_transfer.get(asset.clone()).unwrap_or(0),
);
}

pub fn add_flash_borrow(&mut self, asset: &Address, amount: i128) {
self.flash_borrow.push_back((asset.clone(), amount));
}
}

/// Build a set of pool actions and the new positions from the supplied requests. Validates that the requests
Expand Down Expand Up @@ -190,13 +198,16 @@ pub fn build_actions_from_request(
);
}
RequestType::Borrow => {
let mut reserve = pool.load_reserve(e, &request.address, true);
let d_tokens_minted = reserve.to_d_token_up(request.amount);
from_state.add_liabilities(e, &mut reserve, d_tokens_minted);
reserve.require_utilization_below_max(e);
actions.add_for_pool_transfer(&reserve.asset, request.amount);
check_health = true;
pool.cache_reserve(reserve);
let d_tokens_minted = build_borrow_generic(
e,
pool,
&mut from_state,
&mut actions,
&mut check_health,
request.clone(),
false,
);

e.events().publish(
(
Symbol::new(e, "borrow"),
Expand Down Expand Up @@ -307,6 +318,32 @@ pub fn build_actions_from_request(
(),
);
}
// This is pretty much just a standard borrow that however immediately transfers
// the borrowed amount to [`user`] instead of adding it to the transfer actions.
// It also calls a contract ([`user`]). This step will panic if the provided address
// is not a contract or does not respect the flash loan interface. Both the transfer
// and the invocation don't need to happen within this code block, just before the
// other actions.
RequestType::FlashBorrow => {
let d_tokens_minted = build_borrow_generic(
e,
pool,
&mut from_state,
&mut actions,
&mut check_health,
request.clone(),
true,
);

e.events().publish(
(
Symbol::new(e, "flash_borrow"),
request.address.clone(),
from.clone(),
),
(request.amount, d_tokens_minted),
);
}
}
}

Expand All @@ -316,6 +353,38 @@ pub fn build_actions_from_request(
(actions, from_state, check_health)
}

fn build_borrow_generic(
e: &Env,
pool: &mut Pool,
from_state: &mut User,
actions: &mut Actions,
check_health: &mut bool,
request: Request,
flash: bool,
) -> i128 {
let transfer_amount = request.amount;
let token_address = &request.address;
let mut reserve = pool.load_reserve(e, token_address, true);
let d_tokens_minted = reserve.to_d_token_up(transfer_amount);
from_state.add_liabilities(e, &mut reserve, d_tokens_minted);
reserve.require_utilization_below_max(e);

// the actual difference between the flash borrow and standard borrow.
if !flash {
actions.add_for_pool_transfer(token_address, transfer_amount);
} else {
actions.add_flash_borrow(token_address, transfer_amount);
}
Comment on lines +372 to +377
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be clearer to have the individualized code within the switch statement instead of in this function

*check_health = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should leave this line in the switch statement as well, to follow suit with how other external functions are called, like auction::fill

pool.cache_reserve(reserve);

// we don't directly emit the event here because we may want flashborrow
// and borrow to have distinct event structure. Note that the additional clone
// introduced to support this logic has no effect on cost since it's just a stub.

d_tokens_minted
}

#[cfg(test)]
mod tests {

Expand Down
20 changes: 20 additions & 0 deletions pool/src/pool/submit.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use moderc3156::FlashLoanClient;
use sep_41_token::TokenClient;
use soroban_sdk::{panic_with_error, Address, Env, Vec};

Expand Down Expand Up @@ -48,6 +49,25 @@ pub fn execute_submit(
panic_with_error!(e, PoolError::InvalidHf);
}

// deal with potential flash lending before dealing with the other transfers.
// tbd: do we want to allow for multiple flash loans? There doesn't seem to be a risk in it so currently enabled.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree.

The only real use case I can imagine for this would probably not benefit from multiple contracts being invoked, however.

It seems more likely that a user would flash borrow multiple tokens to 1 contract to then invoke. However, this is would break moderc3156 interface.

IMO we can leave for now, but it's worth some additional thought.

cc: @markuspluna

for (address, amount) in actions.flash_borrow {
// note: TBD if `from` is indeed the receiver contract (it probably isn't? better to not add new args)
TokenClient::new(e, &address).transfer(&e.current_contract_address(), from, &amount);
// calls the receiver contract.
FlashLoanClient::new(&e, from).exec_op(
&e.current_contract_address(),
&address,
&amount,
&0,
);
}

// note: at this point, the pool has sum_by_asset(actions.flash_borrow.1) for each involed asset, but the user also has
// increased liabilities. These will have to be either fully repaid by now in the requests following the flash borrow
// or the user needs to have some previously added collateral to cover the borrow, i.e user is already healthy at this point,
// we just have to make sure that they have the balances they are claiming to have through the transfers.

// transfer tokens from sender to pool
for (address, amount) in actions.spender_transfer.iter() {
TokenClient::new(e, &address).transfer(spender, &e.current_contract_address(), &amount);
Expand Down
Loading