跨链借贷把「抵押在链 A、借款在链 B」或「在某链上开仓并在另一链上获得流动性」变为可能,但也带来了复杂性。下面尽量详尽列出设计要点与工程权衡。
在本课示例中,我们采用 Lock-mint / Credit hybrid 的教学模型(抵押在 ChainA,bridge 发消息到 ChainB mint vToken;借款在 ChainB),因为它直观同时能演示跨链清算问题。
缓解办法:提高 overcollateralization、分段清算、引入 keeper economic incentives(提前支付桥费、承担临时风险)、使用 escrow / bonded relayers。
MockBridge
→ LayerZero/Axelar/Wormhole
:主要改动是消息的验证(签名/attestation)与最终性处理(等待 confirmations / nonce)。在下面的示例中,模拟 两条链(在本地一条 EVM 环境里通过合约部署模拟两链),包含:
MockBridge
:跨链消息中继;CollateralManagerA
:部署在 Chain A,用户存入原始 token,合约锁定并发出桥消息到 Chain B(请求 mint Credit);CreditManagerB
:部署在 Chain B,接收桥消息 mint vToken(代表在 A 锁定的抵押),贷款在 B 发放,借款/负债在 B 记录;MockOracle
:为两个链提供价格(教学会在单地址 mock 返回不同值以模拟价格波动);注意:这是教学级实现,省去了真实桥的复杂签名与 finality 机制,使用
deliverMessage
/deliverSeize
由测试主动调用模拟 relayer / oracle 迟延
src/MockBridge.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title MockBridge
* @notice 教学用的模拟跨链桥合约:发送消息并由管理员(测试环境)手动或自动触发消息传递
* @dev 该合约用于模拟跨链通信,在测试环境中使用
*/
contract MockBridge {
/// @notice 消息发送事件
event MessageSent(uint256 indexed dstChainId, address indexed target, bytes data, uint256 nonce);
/// @notice 消息传递完成事件
event MessageDelivered(uint256 indexed srcChainId, address indexed target, bytes data, uint256 nonce);
/// @notice 消息计数器
uint256 public nonce;
/// @notice 管理员地址
address public admin;
constructor() {
admin = msg.sender;
}
/**
* @notice 发送消息到目标链(模拟)
* @dev 记录事件并递增nonce
* @param dstChainId 目标链ID
* @param target 目标合约地址
* @param data 调用数据
* @return 返回消息nonce
*/
function sendMessage(uint256 dstChainId, address target, bytes calldata data) external returns (uint256) {
nonce++;
emit MessageSent(dstChainId, target, data, nonce);
return nonce;
}
/**
* @notice 传递消息到目标合约
* @dev 仅管理员可调用,用于模拟中继器/证明机制
* @param srcChainId 源链ID
* @param target 目标合约地址
* @param data 调用数据
* @param _nonce 消息nonce
*/
function deliverMessage(uint256 srcChainId, address target, bytes calldata data, uint256 _nonce) external {
// 在生产环境中,这里需要验证签名/证明
(bool ok, ) = target.call(data);
require(ok, "delivery failed");
emit MessageDelivered(srcChainId, target, data, _nonce);
}
/**
* @notice 设置新的管理员地址
* @dev 仅当前管理员可调用,用于测试灵活性
* @param a 新的管理员地址
*/
function setAdmin(address a) external {
require(msg.sender == admin, "not admin");
admin = a;
}
}
src/ICrossBridge.sol
(interface)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title ICrossBridge
* @notice 跨链桥接口定义
*/
interface ICrossBridge {
/**
* @notice 发送消息到目标链
* @param dstChainId 目标链ID
* @param target 目标合约地址
* @param data 调用数据
* @return 返回消息nonce
*/
function sendMessage(uint256 dstChainId, address target, bytes calldata data) external returns (uint256);
}
src/CollateralManagerA.sol
(在 Chain A)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "./ICrossBridge.sol";
/**
* @title ICollateralReceiver
* @notice 抵押品接收器接口
*/
interface ICollateralReceiver {
/**
* @notice 当抵押品铸造时调用
* @param user 用户地址
* @param amount 抵押品数量
* @param srcChainId 源链ID
* @param nonce 消息nonce
*/
function onCollateralMint(address user, uint256 amount, uint256 srcChainId, uint256 nonce) external;
/**
* @notice 当扣押抵押品时调用
* @param user 用户地址
* @param amount 扣押数量
* @param srcChainId 源链ID
* @param nonce 消息nonce
*/
function onSeizeCollateral(address user, uint256 amount, uint256 srcChainId, uint256 nonce) external;
}
/**
* @title CollateralManagerA
* @notice 链A的抵押品管理器:处理抵押品存款和跨链桥接
*/
contract CollateralManagerA {
using SafeERC20 for IERC20;
/// @notice 原始代币(例如 WETH)
IERC20 public immutable underlying;
/// @notice 跨链桥接口
ICrossBridge public bridge;
/// @notice 模拟的链A ID
uint256 public chainId;
/// @notice 用户地址 => 锁定数量 的映射
mapping(address => uint256) public locked;
/// @notice 存款事件
event Deposited(address indexed user, uint256 amount, uint256 nonce);
/// @notice 扣押事件
event Seized(address indexed user, uint256 amount);
/**
* @notice 构造函数
* @param _underlying 原始代币地址
* @param _bridge 跨链桥地址
* @param _chainId 链A ID
*/
constructor(IERC20 _underlying, ICrossBridge _bridge, uint256 _chainId) {
underlying = _underlying;
bridge = _bridge;
chainId = _chainId;
}
/**
* @notice 用户在链A存款抵押品并跨链桥接到链B
* @dev 锁定原始代币并发送消息到链B
* @param dstChainId 目标链ID
* @param creditManagerOnDst 目标链信用管理器地址
* @param amount 存款数量
*/
function depositAndBridge(uint256 dstChainId, address creditManagerOnDst, uint256 amount) external {
require(amount > 0, "zero");
underlying.safeTransferFrom(msg.sender, address(this), amount);
locked[msg.sender] += amount;
// 准备消息调用数据:调用 onCollateralMint(user, amount, srcChainId, nonce)
bytes memory payload = abi.encodeWithSelector(
ICollateralReceiver.onCollateralMint.selector,
msg.sender,
amount,
chainId,
0 // nonce占位符(桥接器返回实际值)
);
uint256 n = bridge.sendMessage(dstChainId, creditManagerOnDst, payload);
emit Deposited(msg.sender, amount, n);
}
/**
* @notice 通过桥接器传递消息扣押抵押品(在跨链清算决策后)
* @dev 任何人都可调用,但在生产环境中应验证证明
* @param user 用户地址
* @param amount 扣押数量
*/
function seize(address user, uint256 amount) external {
// 在生产环境中:验证来自桥接器的跨链决策证明
require(amount <= locked[user], "exceed locked");
locked[user] -= amount;
underlying.safeTransfer(msg.sender, amount); // 将扣押的抵押品发送给清算人(调用者)
emit Seized(user, amount);
}
/**
* @notice 设置桥接器地址(用于测试)
* @dev 生产环境中需要访问控制
* @param b 新的桥接器地址
*/
function setBridge(ICrossBridge b) external {
// 生产环境中需要访问控制
bridge = b;
}
}
src/CreditManagerB.sol
(在 Chain B)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
/**
* @title IBridgeReceiver
* @notice 桥接接收器接口
*/
interface IBridgeReceiver {
/**
* @notice 当抵押品铸造时由桥接器调用
* @param user 用户地址
* @param amount 抵押品数量
* @param srcChainId 源链ID
* @param nonce 消息nonce
*/
function onCollateralMint(
address user,
uint256 amount,
uint256 srcChainId,
uint256 nonce
) external;
/**
* @notice 当扣押抵押品时由桥接器调用
* @param user 用户地址
* @param amount 扣押数量
* @param srcChainId 源链ID
* @param nonce 消息nonce
*/
function onSeizeCollateral(
address user,
uint256 amount,
uint256 srcChainId,
uint256 nonce
) external;
}
/**
* @title CreditManagerB
* @notice 链B的信用管理器:处理vToken铸造、借贷和清算
*/
contract CreditManagerB is IBridgeReceiver {
using SafeERC20 for IERC20;
/// @notice vToken名称(代表抵押品信用的简单ERC20代币)
string public name = "vCollateral";
/// @notice vToken符号
string public symbol = "vCOL";
/// @notice vToken小数位
uint8 public decimals = 18;
/// @notice 地址 => vToken余额 的映射
mapping(address => uint256) public vBalance;
/// @notice vToken总供应量
uint256 public vTotalSupply;
/// @notice 模拟借贷池流动性(稳定币)
IERC20 public stable;
/// @notice 总借款量
uint256 public totalBorrows;
/// @notice 用户地址 => 借款数量 的映射
mapping(address => uint256) public borrows;
/// @notice 预言机地址(模拟,用于检查价格)
address public oracle;
/// @notice 抵押品铸造事件
event CollateralMinted(address indexed user, uint256 amount);
/// @notice 借款事件
event Borrowed(address indexed user, uint256 amount);
/// @notice 清算请求事件
event LiquidationRequested(
address indexed user,
address indexed liquidator,
uint256 repayAmount,
uint256 seizeAmount
);
/**
* @notice 构造函数
* @param _stable 稳定币地址
* @param _oracle 预言机地址
*/
constructor(IERC20 _stable, address _oracle) {
stable = _stable;
oracle = _oracle;
}
/**
* @notice 桥接器调用此函数来铸造vToken,代表在链A锁定的抵押品
* @dev 桥接器传递消息时调用
* @param user 用户地址
* @param amount 抵押品数量
* @param srcChainId 源链ID
* @param nonce 消息nonce
*/
function onCollateralMint(
address user,
uint256 amount,
uint256 /*srcChainId*/,
uint256 /*nonce*/
) external override {
// 为用户铸造vToken(教学用途1:1比例)
vBalance[user] += amount;
vTotalSupply += amount;
emit CollateralMinted(user, amount);
}
/**
* @notice 桥接器调用此函数来扣押链A上的抵押品
* @dev 桥接器传递消息时调用
* @param user 用户地址
* @param amount 扣押数量
* @param srcChainId 源链ID
* @param nonce 消息nonce
*/
function onSeizeCollateral(
address user,
uint256 amount,
uint256 /*srcChainId*/,
uint256 /*nonce*/
) external override {
// 在链B上减少用户的vToken余额(相当于清算时销毁vToken)
require(vBalance[user] >= amount, "insufficient vBalance");
vBalance[user] -= amount;
vTotalSupply -= amount;
// 注意:这里不转移实际资产,因为实际资产在链A上
}
/**
* @notice 使用vToken作为抵押品在链B上借款(教学用途:简单LTV固定比例)
* @dev 简单的抵押率检查:vBalance * 价格 * 系数 >= 借款价值
* @param amount 借款数量
*/
function borrow(uint256 amount) external {
// 需要简单的抵押品检查:vBalance * 价格 * 系数 >= 借款价值
// 在测试中通过外部调用预言机来控制价格
// 简化:1 vToken = 1个基础单位,价格由预言机提供
// 生产环境:使用稳健的预言机 + 价格单位标准化
require(vBalance[msg.sender] > 0, "no vcollateral");
// 简单检查:允许借款最多为vBalance的75%
uint256 maxBorrow = (vBalance[msg.sender] * 75) / 100;
require(amount <= maxBorrow, "exceed LTV");
borrows[msg.sender] += amount;
totalBorrows += amount;
stable.safeTransfer(msg.sender, amount);
emit Borrowed(msg.sender, amount);
}
/**
* @notice 链B上的清算人偿还用户的部分借款并请求扣押链A上的抵押品
* @dev 由清算人调用;通过桥接器发送消息(在测试中模拟)
* @param user 用户地址
* @param repayAmount 偿还数量
* @param bridge 桥接器地址
* @param dstChainId 目标链ID
* @param collateralManagerOnA 链A抵押品管理器地址
*/
function liquidate(
address user,
uint256 repayAmount,
address bridge,
uint256 dstChainId,
address collateralManagerOnA
) external {
require(borrows[user] >= repayAmount, "repay > debt");
// 将偿还的稳定币从清算人转移到本合约(资金池)
stable.safeTransferFrom(msg.sender, address(this), repayAmount);
borrows[user] -= repayAmount;
totalBorrows -= repayAmount;
// 计算vToken的扣押数量(应用清算奖励,例如5%)
uint256 seizeAmount = (repayAmount * 105) / 100; // 简单的1:1价值假设
// 准备发送给CollateralManagerA.seize(user, seizeAmount)的消息
// bytes memory payload = abi.encodeWithSelector(
// CollateralManagerAInterface.seize.selector,
// user,
// seizeAmount
// );
// 通过桥接器发送(实际调用将在测试中构造)
// 我们无法在此处调用桥接器,因为我们保持桥接器不可知。测试将发送消息。
emit LiquidationRequested(user, msg.sender, repayAmount, seizeAmount);
}
/**
* @notice 查询用户的vToken余额
* @param user 用户地址
* @return vToken余额
*/
function vBalanceOf(address user) external view returns (uint256) {
return vBalance[user];
}
}
/**
* @title CollateralManagerAInterface
* @notice 链A抵押品管理器接口
*/
interface CollateralManagerAInterface {
/**
* @notice 扣押用户抵押品
* @param user 用户地址
* @param amount 扣押数量
*/
function seize(address user, uint256 amount) external;
}
注意:上面合约故意保持教学简洁:
CreditManagerB.liquidate
只发出事件LiquidationRequested
(在真实场景中,该合约会调用桥sendMessage
将seize
请求发送回 Chain A)。为了把桥耦合最小化,我们把 bridge 操作放到测试驱动中,由测试模拟 relayer 把消息交付到CollateralManagerA.seize
。
下面的测试整体在单一 EVM 环境运行——我们用不同合约实例来模拟 Chain A / Chain B,使用 MockBridge
的 sendMessage
+ deliverMessage
模拟消息传递(由测试充当 relayer/admin)。
test/CrossChainLending.t.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/MockBridge.sol";
import "../src/CollateralManagerA.sol";
import "../src/CreditManagerB.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
/**
* @title MockToken
* @notice 模拟ERC20代币合约,用于测试
*/
contract MockToken is ERC20 {
constructor(string memory n, string memory s) ERC20(n, s) {}
/**
* @notice 铸造代币
* @param to 接收地址
* @param amt 铸造数量
*/
function mint(address to, uint256 amt) external {
_mint(to, amt);
}
}
/**
* @title CrossChainLendingTest
* @notice 跨链借贷测试合约
* @dev 使用Forge测试框架进行跨链借贷流程的端到端测试
*/
contract CrossChainLendingTest is Test {
MockBridge bridge;
MockToken underlying; // 链A的代币
MockToken stable; // 链B的代币(借款资产)
CollateralManagerA collA;
CreditManagerB credB;
address user = address(0x1);
address liquidator = address(0x2);
function setUp() public {
bridge = new MockBridge();
underlying = new MockToken("Underlying", "u");
stable = new MockToken("Stable", "s");
// 部署链A抵押品管理器,链ID = 1
collA = new CollateralManagerA(IERC20(address(underlying)), ICrossBridge(address(bridge)), 1);
// 部署链B信用管理器
credB = new CreditManagerB(IERC20(address(stable)), address(0)); // 教学用途未使用预言机
// 准备余额
underlying.mint(user, 10 ether);
stable.mint(address(credB), 10000 ether); // 链B的流动性
stable.mint(liquidator, 2000 ether);
// 用户授权存款
vm.startPrank(user);
underlying.approve(address(collA), type(uint256).max);
vm.stopPrank();
}
/**
* @notice 测试完整的存款、跨链桥接、借款和清算流程
* @dev 验证跨链借贷系统的端到端功能
*/
function testDepositBridgeBorrowAndLiquidationFlow() public {
// 1) 用户在链A存款并通过桥接器在链B铸造vToken
vm.startPrank(user);
collA.depositAndBridge(2, address(credB), 5 ether); // 目标链ID=2,信用管理器地址
vm.stopPrank();
// 桥接器产生MessageSent事件;现在模拟中继器将消息传递给credB
// 为此,我们重建匹配onCollateralMint签名的payload
bytes memory payload = abi.encodeWithSelector(
ICollateralReceiver.onCollateralMint.selector,
user,
5 ether,
uint256(1),
uint256(1)
);
// 中继器(桥接器管理员)将消息传递给credB
bridge.deliverMessage(1, address(credB), payload, 1);
// 验证链B上铸造的vToken(vBalance)
assertEq(credB.vBalanceOf(user), 5 ether);
// 2) 用户在链B上借款,最高75% LTV(教学用途)
vm.startPrank(user);
// 确保稳定币有足够流动性:已在setUp中铸造给credB
// 用户借款3.5(<= 5 * 0.75 = 3.75)
uint256 borrowAmount = 3500000000000000000; // 3.5 ether
credB.borrow(borrowAmount);
vm.stopPrank();
assertEq(credB.borrows(user), borrowAmount);
// 3) 通过减少链B上有效抵押品价值来模拟价格冲击(我们通过让清算人行动来模拟)
// 在这个教学设置中,我们跳过预言机;我们继续让清算人偿还部分债务以请求扣押。
vm.startPrank(liquidator);
stable.approve(address(credB), type(uint256).max);
// 修复:还款金额应该小于等于用户债务
// 用户借了 3.5 ether,我们让清算者偿还 1.5 ether(而不是之前的 1500 ether)
uint256 repayAmount = 1500000000000000000; // 1.5 ether
credB.liquidate(user, repayAmount, address(bridge), 1, address(collA));
vm.stopPrank();
// 现在用户的债务应该减少了
assertEq(credB.borrows(user), borrowAmount - repayAmount);
// 现在模拟中继器将扣押消息传递给 collA
// 扣押金额 = 还款金额 * 1.05 = 1.5 ether * 1.05 = 1.575 ether
uint256 seizeAmount = (repayAmount * 105) / 100;
bytes memory seizePayload = abi.encodeWithSelector(
CollateralManagerA.seize.selector,
user,
seizeAmount
);
// 传递扣押消息:桥管理员调用 deliverMessage
bridge.deliverMessage(2, address(collA), seizePayload, 2);
// 现在 collA.locked[user] 应该减少了(初始 5 - 扣押 ~1.575 = ~3.425)
uint256 left = collA.locked(user);
assertEq(left, 5 ether - seizeAmount);
// 验证清算者的稳定币余额变化
// 清算者支付了 1.5 ether 稳定币来偿还债务
uint256 liquidatorStableBalanceAfter = stable.balanceOf(liquidator);
assertEq(liquidatorStableBalanceAfter, 2000 ether - repayAmount);
// 验证 CreditManagerB 的稳定币余额变化
uint256 credBStableBalanceAfter = stable.balanceOf(address(credB));
assertEq(credBStableBalanceAfter, 10000 ether - borrowAmount + repayAmount);
}
}
测试说明:
我们在测试中模拟 relayer:
bridge.deliverMessage
由测试调用,代表桥的 attestation/relay 完成。CreditManagerB.liquidate
在教学实现中只是发出事件,我们在测试手工构造 seizePayload 并由 bridge.deliverMessage 调用CollateralManagerA.seize
实际扣押抵押并发送给清算人(调用者)。 真实系统应把 bridge 发消息这一环节放在CreditManagerB.liquidate
(由它调用bridge.sendMessage
),并在链 A 的CollateralManagerA.seize
做更严格的 attestation 验证(nonce / signature / source chain / governance control)。
执行测试:
➜ defi git:(master) forge test --match-path test/CrossChainLending.t.sol -vvv
[⠊] Compiling...
[⠆] Compiling 1 files with Solc 0.8.30
[⠔] Solc 0.8.30 finished in 1.27s
Compiler run successful!
Ran 1 test for test/CrossChainLending.t.sol:CrossChainLendingTest
[PASS] testDepositBridgeBorrowAndLiquidationFlow() (gas: 354312)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.98ms (715.34µs CPU time)
Ran 1 test suite in 328.14ms (1.98ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
CreditManagerB.liquidate
中直接调用 bridge.sendMessage(...)
来发送 seize 请求,并在 MockBridge.deliverMessage
处自动执行(现在测试是手动调用)。写测试验证 end-to-end。原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。