Solidity閃電貸實現方式與Move以及Rust閃電貸實現方式有何不同?

2023-10-26 10:17 Beosin


作者:Beosin安全研究專家Sivan

閃電貸是一種無抵押借款的服務,由於其擁有無需抵押便能借出資金的特性,使得資金利用率大大提高。在常見的以太坊閃電貸中,是通過以太坊交易機制來保證可以進行無抵押借出資金,以太坊中一個交易可以包含很多步驟,如:借款、兌換、使用、還款等,所有的步驟相輔相成,若其中某一個或多個步驟出現錯誤,都將導致本次的整個交易被回滾。

隨着區塊鏈生態發展,出現了大量公鏈以及合約編程語言,例如:除了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 能力詳情:

https://move-book.com/advanced-topics/types-with-abilities.html

因此在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每個指令都是原子類型,並且支持指令自省,因此使用指令自省的方式實現閃電貸是較好的方式。

鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播信息之目的,不構成任何投資建議,如有侵權行為,請第一時間聯絡我們修改或刪除,多謝。

標題:Solidity閃電貸實現方式與Move以及Rust閃電貸實現方式有何不同?

地址:https://www.sgitmedia.com/article/13872.html

相關閱讀: