Solidity闪电贷实现方式,与Move、Rust有何不同?

对比三种语言的闪电贷流程,由於语言的特性,在实现方式上有所不同。
(前情提要:Beosin:Move VM先前毁灭级漏洞,可让Sui、Aptos「崩溃、甚至硬分叉」 )
(背景补充:Metamask开发公司ConsenSys:给 Solidity 开发者的 16 个安全建议 )

本文目录

闪电贷是一种无抵押借款的服务,由於其拥有无需抵押便能借出资金的特性,使得资金利用率大大提高。在常见的以太坊闪电贷中,是通过以太坊交易机制来保证可以进行无抵押借出资金,以太坊中一个交易可以包含很多步骤,如:借款、兑换、使用、还款等,所有的步骤相辅相成,若其中某一个或多个步骤出现错误,都将导致本次的整个交易被回滚。

随着区块链生态发展,出现了大量公链以及合约程式语言,例如:除了 Solidity 之外最常见的 Move 和 Rust,这些合约程式语言有本质上的区别,框架与程式设计理念也有所不同, 本篇文章我们来对比一下 Solidity 闪电贷实现方式与 Move 以及 Rust 闪电贷实现方式有何不同,同时可以初步了解一下各种语言的程式设计理念。

延伸阅读:解析闪电贷》今年DeFi被盗最高破1.2亿镁,去中心化金融成「骇客」敛财神器?

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 {

flash_lender_id: ID,

repay_amount: u64

}

public fun loan(self: &mut FlashLender, amount: u64, ctx: &mut TxContext):

(Coin, Receipt) {

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(self: &mut FlashLender, payment: Coin, receipt: Receipt) {

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, 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 tokens

pub fn repay(ctx: Context, 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 每个指令都是原子型别,并且支援指令自省,因此使用指令自省的方式实现闪电贷是较好的方式。

📍相关报导📍

 SUI 遭爆3月秘密修复「十亿美元安全漏洞」,可让骇客闪电贷攻击..

闪电网路爆重大安全漏洞!比特币核心开发者退出开发:从底层改才有救

Visa抢聘区块链工程师!须懂L1、L2,精熟Solidity与Rust语言

Leave a Reply

Your email address will not be published. Required fields are marked *