對比三種語言的閃電貸流程,均為借款-\x26gt; 使用-\x26gt; 還款三步,只是由於語言的特性,在實現方式上有所不同。
作者:Sivan,Beosin 安全研究專家
封面:Photo by Abdullah Ahmad on Unsplash
閃電貸是一種無抵押借款的服務,由於其擁有無需抵押便能借出資金的特性,使得資金利用率大大提高。 在常見的乙太坊閃電貸中,是通過乙太坊交易機制來保證可以進行無抵押借出資金,乙太坊中一個交易可以包含很多步驟,如:借款、兌換、使用、還款等,所有的步驟相輔相成,若其中某一個或多個步驟出現錯誤,都將導致本次的整個交易被回滾。
隨著區塊鏈生態發展,出現了大量公鏈以及合約程式設計語言,例如:除了 Solidity 之外最常見的 Move 和 Rust,這些合約程式設計語言有本質上的區別,框架與程式設計理念也有所不同,本篇文章我們來對比一下 Solidity 閃電貸實現方式與 Move 以及 Rust 閃電貸實現方式有何不同,同時可以初步瞭解一下各種語言的程式設計理念。
Solidity 相關閃電貸:
Solidity 的閃電貸是基於 Solidity 支援動態調用這一特性來設計的,何為動態調用,也就是 solidity 支援在調用一個函數的過程中,動態傳入需要調用的位址,如下例代碼。 每次調用都可以傳入不同的位址,根據這個特點,便出現了 solidity 閃電貸的實現邏輯。
function callfun(address addr) public { addr.call();}
如下代碼,將閃電貸抽象成了 3 個核心功能,
1、首先直接將資金發送給調用者;
2、再調用調用者合約,從而讓調用者使用這些資金;
3、調用者使用結束,檢查是否歸還資金以及手續費,如果檢查失敗則回滾交易。(此處也可以直接使用 transferfrom 函數將調用則資金轉移回來)
function flashloan(uint amount, address to) { transfer( to, amount); // 发送资金给调用者 to.call();//调用调用者的合约函数 check();//检查是否归还资金}
如下圖,為 Solidity 語言中閃電貸的實現流程:
下列代碼為真實專案 Uniswap 閃電貸邏輯。 代碼範例:
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT'); (uint112 _reserve0, uint112 _reserve1,) = getReserves(); require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY'); uint balance0; uint balance1; { address _token0 = token0; address _token1 = token1; require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO'); /**将资金转给用户**/ if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);
/**调用用户指定的目标函数**/ if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); balance0 = IERC20(_token0).balanceOf(address(this)); balance1 = IERC20(_token1).balanceOf(address(this)); } uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0; uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0; require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT'); { uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3)); uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3)); /**检查用户是否归还资金以及手续费**/ require(balance0Adjusted.mul(balance1Adjusted)>=uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
} _update(balance0, balance1, _reserve0, _reserve1); emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);}
Move 相關閃電貸:
Move 閃電貸和 solidity 設計思想不同,move 中沒有動態調用這一個特性,在所有函數調用過程之前,都必須確定調用流程,明確調用合約地址是什麼,所以無法像 solidity 裡面那樣動態傳入位址再進行調用。
那麼 move 能實現閃電貸功能嗎? 當然可以,move 的特性使得人們設計出與 solidity 實現方式不同的閃電貸。
在 Move 中,將數據和執行代碼分離,造就了 Move VM 獨特的資源-模組模型。 在這種模型中,不允許資源在交易結束時未被銷毀或者保存在全域存儲中,因此 Move 中的資源存在一種特殊的結構體——燙手山芋(Hot Potato),它是一個沒有任何能力修飾符的結構體,因此它只能在其模組中被打包和解包。
*Move 能力詳情:
因此在 move 語言中的閃電貸實現,巧妙地利用了這種模式,將閃貸和還款操作抽象為兩個函數進行處理,中間產生借貸資源記錄借貸情況,該資源並沒任何能力,只能夠在還款函數中通過解包的方式將借貸資源給消耗掉,因此借貸操作必須和還款操作綁定在同一個操作中,否則閃電貸交易就會失敗。
如下圖,為 move 語言中閃電貸的實現流程。
如下代碼,loan 與 repay 兩個函數相結合便可以實現閃電貸。 需要使用閃電貸服務的使用者,先調用 loan 函數申請借款。 函數會首先判斷是否有足夠的資金提供借款,隨後將資金發送給調用者,計算好費用后,創建一個沒有任何能力的資源 “receipt” 並返回給調用者。 調用者在自己的合約中使用借貸的資金,最後需要將 “receipt” 返還到 repay 函數,並且附帶歸還的資金。 在 repay 函數中,首先將 “receipt” 資源解構,以確保交易成功執行,隨後判斷使用者歸還資金是否與之前計算好的資金數量相同,最後完成整個交易。
代碼範例:
struct Receipt<phantom T> { flash_lender_id: ID, repay_amount: u64}public fun loan<T>(self: &mut FlashLender<T>, amount: u64, ctx: &mut TxContext): (Coin<T>, Receipt<T>) { let to_lend = &mut self.to_lend; assert!(balance::value(to_lend) >= amount, ELoanTooLarge); let loan = coin::take(to_lend, amount, ctx); let repay_amount = amount + self.fee; let receipt = Receipt { flash_lender_id: object::id(self), repay_amount }; (loan, receipt)}public fun repay<T>(self: &mut FlashLender<T>, payment: Coin<T>, receipt: Receipt<T>) { let Receipt { flash_lender_id, repay_amount } = receipt; assert!(object::id(self) == flash_lender_id, ERepayToWrongLender); assert!(coin::value(&payment) == repay_amount, EInvalidRepaymentAmount); coin::put(&mut self.to_lend, payment)}
Rust 相關閃電貸:
Rust 由於其提供記憶體安全、併發安全和零成本抽象等特性。 也被用在了區塊鏈智慧合約語言開發中,接下來我們以 Solana 智慧合約(Program)為例講解使用 Rust 開發實現的閃電貸。
Solana VM 亦將數據和執行代碼進行了分離,使得一份執行代碼可以處理多份數據副本,但與 Move 不同的是,數位帳戶是通過程式派生的方式完成的,並且沒有類似於 Move 特性的限制。 因此 Solana Rust 不能夠使用 Move 的方式實現閃電貸,並且 Solana Rust 動態調用指令(等同於理解為合約的函數)遞歸深度限制為 4,使用 Solidity 動態調用的方式同樣不可取。 但在 Solana 中每個指令(instruction)調用在交易中是原子類型的,因此在一筆交易中可以在一個指令中檢查是否存在另一個指令。 而 Solana 中的閃電貸依賴此了特性,Solana 閃電貸在閃貸的指令中將檢查閃電貸交易中是否存在還款的指令,並檢查還款的數量是否正確。
如下圖,為 Rust 語言中閃電貸的實現流程:
代碼範例:
pub fn borrow(ctx: Context<Borrow>, amount: u64) -> ProgramResult { msg!("adobe borrow"); if ctx.accounts.pool.borrowing { return Err(AdobeError::Borrowing.into()); } let ixns = ctx.accounts.instructions.to_account_info(); // make sure this isnt a cpi call let current_index = solana::sysvar::instructions::load_current_index_checked(&ixns)? as usize; let current_ixn = solana::sysvar::instructions::load_instruction_at_checked(current_index, &ixns)?; if current_ixn.program_id != *ctx.program_id { return Err(AdobeError::CpiBorrow.into()); }
let mut i = current_index + 1; loop { // 遍历交易序列中的指令, if let Ok(ixn) = solana::sysvar::instructions::load_instruction_at_checked(i, &ixns) { // 查找是否同时调用了该程序的中还款指令(repay) if ixn.program_id == *ctx.program_id // 检查 invoke data 中 函数签名 && u64::from_be_bytes(ixn.data[..8].try_into().unwrap()) == REPAY_OPCODE && ixn.accounts[2].pubkey == ctx.accounts.pool.key() { // 检查 函数 invoke data 中 amount 数量是否正确 if u64::from_le_bytes(ixn.data[8..16].try_into().unwrap()) == amount { break; } else { return Err(AdobeError::IncorrectRepay.into()); } } else { i += 1; } }else { return Err(AdobeError::NoRepay.into()); } } let state_seed: &[&[&[u8]]] = &[&[ &State::discriminator()[..], &[ctx.accounts.state.bump], ]]; let transfer_ctx = CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.pool_token.to_account_info(), to: ctx.accounts.user_token.to_account_info(), authority: ctx.accounts.state.to_account_info(), }, state_seed, ); // cpi 转账 token::transfer(transfer_ctx, amount)?; ctx.accounts.pool.borrowing = true; Ok(())}
// REPAY// receives tokenspub fn repay(ctx: Context<Repay>, amount: u64) -> ProgramResult { msg!("adobe repay"); let ixns = ctx.accounts.instructions.to_account_info(); // make sure this isnt a cpi call let current_index = solana::sysvar::instructions::load_current_index_checked(&ixns)? as usize; let current_ixn = solana::sysvar::instructions::load_instruction_at_checked(current_index, &ixns)?; if current_ixn.program_id != *ctx.program_id { return Err(AdobeError::CpiRepay.into()); } let state_seed: &[&[&[u8]]] = &[&[ &State::discriminator()[..], &[ctx.accounts.state.bump], ]];
let transfer_ctx = CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), Transfer { from: ctx.accounts.user_token.to_account_info(), to: ctx.accounts.pool_token.to_account_info(), authority: ctx.accounts.user.to_account_info(), }, state_seed, );
// 还款 token::transfer(transfer_ctx, amount)?;
// 更新账本状态 ctx.accounts.pool.borrowing = false;
Ok(())}
对比三种语言的闪电贷流程,均为借款-> 使用-> 还款三步,只是由于语言的特性,在实现方式上有所不同。
Solidity 支持动态调用,所以可以在单个函数中完成整个交易;
Move 不支持動態調用,由於資源的特性,需要使用兩個函數進行借款和還款邏輯;
Rust(Solana)能支援動態調用,但是僅支援 4 層 CPI 調用,使用 CPI 實現閃電貸將產生局限性,但是 Solana 每個指令都是原子類型,並且支援指令自省,因此使用指令自省的方式實現閃電貸是較好的方式
免責聲明:作為區塊鏈資訊平臺,本站所發佈文章僅代表作者及嘉賓個人觀點,與 Web3Caff 立場無關。 本文內容僅用於資訊分享,均不構成任何投資建議及要約,並請您遵守所在國家或地區的相關法律法規。