Solidity闪电贷实现方式与Move以及Rust闪电贷实现方式有何不同?
作者: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 tokens
pub 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每个指令都是原子类型,并且支持指令自省,因此使用指令自省的方式实现闪电贷是较好的方式。
Bitcoin Price Consolidates Below Resistance, Are Dips Still Supported?
Bitcoin Price Consolidates Below Resistance, Are Dips Still Supported?
XRP, Solana, Cardano, Shiba Inu Making Up for Lost Time as Big Whale Transaction Spikes Pop Up
XRP, Solana, Cardano, Shiba Inu Making Up for Lost Time as Big Whale Transaction Spikes Pop Up
Justin Sun suspected to have purchased $160m in Ethereum
Justin Sun suspected to have purchased $160m in Ethereum