wormhole-lending-examples/example-2/evm/test/helpers/TestHelpers.sol

678 lines
27 KiB
Solidity

// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "../../src/libraries/external/BytesLib.sol";
import {Hub} from "../../src/contracts/lendingHub/Hub.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IWormhole} from "../../src/interfaces/IWormhole.sol";
import {ITokenBridge} from "../../src/interfaces/ITokenBridge.sol";
import {Spoke} from "../../src/contracts/lendingSpoke/Spoke.sol";
import {TestStructs} from "./TestStructs.sol";
import {TestState} from "./TestState.sol";
import {TestSetters} from "./TestSetters.sol";
import {TestGetters} from "./TestGetters.sol";
import {TestUtilities} from "./TestUtilities.sol";
import "../../src/contracts/lendingHub/HubGetters.sol";
import {WormholeSimulator} from "./WormholeSimulator.sol";
contract TestHelpers is TestStructs, TestState, TestGetters, TestSetters, TestUtilities {
using BytesLib for bytes;
function testSetUp(Vm vm) internal {
// this will be used to sign wormhole messages
uint256 guardianSigner = uint256(vm.envBytes32("TESTING_DEVNET_GUARDIAN"));
WormholeSimulator wormholeSimulator =
new WormholeSimulator(vm.envAddress("TESTING_WORMHOLE_ADDRESS_MUMBAI"), guardianSigner);
// we may need to interact with Wormhole throughout the test
IWormhole wormholeContract = wormholeSimulator.wormhole();
// verify Wormhole state from fork
require(wormholeContract.chainId() == uint16(vm.envUint("TESTING_WORMHOLE_CHAINID_MUMBAI")), "wrong chainId");
require(wormholeContract.messageFee() == vm.envUint("TESTING_WORMHOLE_MESSAGE_FEE_MUMBAI"), "wrong messageFee");
require(
wormholeContract.getCurrentGuardianSetIndex() == uint32(vm.envUint("TESTING_WORMHOLE_GUARDIAN_SET_INDEX_MUMBAI")),
"wrong guardian set index"
);
// set up Token Bridge
ITokenBridge tokenBridgeContract = ITokenBridge(vm.envAddress("TESTING_TOKEN_BRIDGE_ADDRESS_MUMBAI"));
// verify Token Bridge state from fork
require(tokenBridgeContract.chainId() == uint16(vm.envUint("TESTING_WORMHOLE_CHAINID_MUMBAI")), "wrong chainId");
// initialize Hub contract
uint8 wormholeFinality = 1;
uint256 interestAccrualIndexPrecision = 10 ** 6;
uint256 collateralizationRatioPrecision = 10 ** 6;
uint256 maxLiquidationBonus = 125 * 10**4;
uint256 maxLiquidationPortion = 50;
uint256 maxLiquidationPortionPrecision = 10 ** 2;
uint8 oracleMode = 1;
uint64 priceStandardDeviations = 424;
uint64 priceStandardDeviationsPrecision = 10 ** 2;
address pythAddress = vm.envAddress("TESTING_PYTH_ADDRESS_MUMBAI");
Hub hub = new Hub(
address(wormholeContract),
address(tokenBridgeContract),
wormholeFinality,
pythAddress,
oracleMode,
priceStandardDeviations,
priceStandardDeviationsPrecision,
maxLiquidationBonus,
maxLiquidationPortion,
maxLiquidationPortionPrecision,
interestAccrualIndexPrecision,
collateralizationRatioPrecision
);
setOracleMode(oracleMode);
setHubData(HubData({
guardianSigner: guardianSigner,
wormholeSimulator: wormholeSimulator,
wormholeContract: wormholeContract,
tokenBridgeContract: tokenBridgeContract,
hub: hub,
hubChainId: wormholeContract.chainId()
}));
setVm(vm);
setPublishTime(1);
registerChainOnHub(uint16(vm.envUint("TESTING_WORMHOLE_CHAINID_MUMBAI")), bytes32(uint256(uint160(vm.envAddress("TESTING_TOKEN_BRIDGE_ADDRESS_MUMBAI")))));
}
function doRegisterSpoke(uint256 index) internal returns (Spoke) {
SpokeData memory spokeData = getSpokeData(index);
// register asset
getHub().registerSpoke(
spokeData.foreignChainId, address(spokeData.spoke)
);
return spokeData.spoke;
}
function doRegisterAsset(Asset memory asset) internal {
Vm vm = getVm();
uint256 reservePrecision = 1 * 10**6;
// register asset
vm.recordLogs();
getHub().registerAsset(
asset.assetAddress,
asset.collateralizationRatioDeposit,
asset.collateralizationRatioBorrow,
asset.ratePrecision,
asset.kinks,
asset.rates,
asset.reserveFactor,
reservePrecision,
asset.pythId
);
AssetInfo memory info = getHub().getAssetInfo(asset.assetAddress);
bool kinksMatch = true;
bool ratesMatch = true;
require(info.interestRateModel.kinks.length == asset.kinks.length, "lengths of kinks arrays don't match");
require(info.interestRateModel.rates.length == asset.rates.length, "lengths of rates arrays don't match");
for(uint i=0; i < asset.kinks.length; i++) {
if (info.interestRateModel.kinks[i] != asset.kinks[i]) {
kinksMatch = false;
}
}
for(uint i=0; i < asset.rates.length; i++) {
if (info.interestRateModel.rates[i] != asset.rates[i]) {
ratesMatch = false;
}
}
require(
(info.collateralizationRatioDeposit == asset.collateralizationRatioDeposit) && (info.collateralizationRatioBorrow == asset.collateralizationRatioBorrow) && (info.decimals == asset.decimals) && (info.pythId == asset.pythId) && (info.exists) && (info.interestRateModel.ratePrecision == asset.ratePrecision) && (kinksMatch) && (ratesMatch) && (info.interestRateModel.reserveFactor == asset.reserveFactor) && (info.interestRateModel.reservePrecision == reservePrecision),
"didn't register properly"
);
}
function doAction(ActionParameters memory params) internal {
Action action = Action(params.action);
bool isNative = params.action == Action.DepositNative || params.action == Action.RepayNative;
Spoke spoke = getSpoke(params.spokeIndex);
address vault = address(this);
if(params.prank) {
vault = params.prankAddress;
}
if(getDebug()) {
console.log("-[Vault %s]--------------", vault);
}
Vm vm = getVm();
ActionStateData memory beforeData = getActionStateData(vault, params.assetAddress, isNative);
if(action == Action.Deposit || action == Action.Repay) {
if(params.prank) {
vm.prank(vault);
}
IERC20(params.assetAddress).approve(address(spoke), params.assetAmount);
}
if(params.prank) {
vm.prank(vault);
}
vm.recordLogs();
if(action == Action.Deposit) {
if(getDebug()) {
console.log("Depositing %s of asset %s", params.assetAmount, getAssetIndex(params.assetAddress));
}
spoke.depositCollateral(params.assetAddress, params.assetAmount);
}
else if(action == Action.Repay) {
if(getDebug()) {
console.log("Repaying %s of asset %s", params.assetAmount, getAssetIndex(params.assetAddress));
}
spoke.repay(params.assetAddress, params.assetAmount);
}
else if(action == Action.Borrow) {
if(getDebug()) {
console.log("Borrowing %s of asset %s", params.assetAmount, getAssetIndex(params.assetAddress));
}
spoke.borrow(params.assetAddress, params.assetAmount);
}
else if(action == Action.Withdraw) {
if(getDebug()) {
console.log("Withdrawing %s of asset %s", params.assetAmount, getAssetIndex(params.assetAddress));
}
spoke.withdrawCollateral(params.assetAddress, params.assetAmount);
} else if(action == Action.DepositNative) {
if(getDebug()) {
console.log("Depositing %s of native token", params.assetAmount);
}
spoke.depositCollateralNative{value: params.assetAmount}();
}
else if(action == Action.RepayNative) {
if(getDebug()) {
console.log("Repaying %s of native token", params.assetAmount);
}
spoke.repayNative{value: params.assetAmount}();
}
Vm.Log[] memory entries = vm.getRecordedLogs();
bytes memory encodedMessage = fetchSignedMessageFromSpokeLogs(params.spokeIndex, entries[entries.length - 1]);
if(params.expectRevert) {
vm.expectRevert(bytes(params.revertString));
}
if(action == Action.Deposit || action == Action.DepositNative) {
getHub().completeDeposit(encodedMessage);
}
else if(action == Action.Repay || action == Action.RepayNative) {
getHub().completeRepay(encodedMessage);
}
else if(action == Action.Borrow) {
getHub().completeBorrow(encodedMessage);
}
else if(action == Action.Withdraw) {
getHub().completeWithdraw(encodedMessage);
}
if(params.expectRevert) {
if(getDebug()) {
console.log("should revert");
console.log("----------------------------------------");
console.log("");
}
return;
}
if(action == Action.Borrow || action == Action.Withdraw || params.paymentReversion) {
entries = vm.getRecordedLogs();
encodedMessage = fetchSignedMessageFromHubLogs(entries[entries.length - 1]);
spoke.tokenBridge().completeTransfer(encodedMessage);
}
ActionStateData memory afterData = getActionStateData(vault, params.assetAddress, isNative);
uint256 amount = params.assetAmount;
if(isNative) amount = amount - getHubData().wormholeContract.messageFee();
requireActionDataValid(action, params.assetAddress, amount, beforeData, afterData, params.paymentReversion);
if(getDebug()) {
console.log("----------------------------------------");
console.log("");
}
}
function doDeposit(uint256 spokeIndex, Asset memory asset, uint256 assetAmount) internal {
doAction(ActionParameters({
action: Action.Deposit,
spokeIndex: spokeIndex,
assetAddress: asset.assetAddress,
assetAmount: assetAmount,
expectRevert: false,
revertString: "",
paymentReversion: false,
prank: false,
prankAddress: address(0x0)
}));
}
function doDeposit(uint256 spokeIndex, Asset memory asset, uint256 assetAmount, address vault) internal {
doAction(ActionParameters({
action: Action.Deposit,
spokeIndex: spokeIndex,
assetAddress: asset.assetAddress,
assetAmount: assetAmount,
expectRevert: false,
revertString: "",
paymentReversion: false,
prank: true,
prankAddress: vault
}));
}
function doDepositRevert(uint256 spokeIndex, Asset memory asset, uint256 assetAmount, string memory revertString) internal {
doAction(ActionParameters({
action: Action.Deposit,
spokeIndex: spokeIndex,
assetAddress: asset.assetAddress,
assetAmount: assetAmount,
expectRevert: true,
revertString: revertString,
paymentReversion: false,
prank: false,
prankAddress: address(0x0)
}));
}
function doDepositNative(uint256 spokeIndex, uint256 amount) internal {
doAction(ActionParameters({
action: Action.DepositNative,
spokeIndex: spokeIndex,
assetAddress: address(getHubData().tokenBridgeContract.WETH()),
assetAmount: amount,
expectRevert: false,
revertString: "",
paymentReversion: false,
prank: false,
prankAddress: address(0x0)
}));
}
function doDepositNative(uint256 spokeIndex, uint256 amount, address vault) internal {
doAction(ActionParameters({
action: Action.DepositNative,
spokeIndex: spokeIndex,
assetAddress: address(getHubData().tokenBridgeContract.WETH()),
assetAmount: amount,
expectRevert: false,
revertString: "",
paymentReversion: false,
prank: true,
prankAddress: vault
}));
}
function doDepositNativeRevert(uint256 spokeIndex, uint256 amount, string memory revertString) internal {
doAction(ActionParameters({
action: Action.DepositNative,
spokeIndex: spokeIndex,
assetAddress: address(getHubData().tokenBridgeContract.WETH()),
assetAmount: amount,
expectRevert: true,
revertString: revertString,
paymentReversion: false,
prank: false,
prankAddress: address(0x0)
}));
}
function doRepay(uint256 spokeIndex, Asset memory asset, uint256 assetAmount) internal {
doAction(ActionParameters({
action: Action.Repay,
spokeIndex: spokeIndex,
assetAddress: asset.assetAddress,
assetAmount: assetAmount,
expectRevert: false,
revertString: "",
paymentReversion: false,
prank: false,
prankAddress: address(0x0)
}));
}
function doRepay(uint256 spokeIndex, Asset memory asset, uint256 assetAmount, address prankAddress) internal {
doAction(ActionParameters({
action: Action.Repay,
spokeIndex: spokeIndex,
assetAddress: asset.assetAddress,
assetAmount: assetAmount,
expectRevert: false,
revertString: "",
paymentReversion: false,
prank: true,
prankAddress: prankAddress
}));
}
function doRepayRevert(uint256 spokeIndex, Asset memory asset, uint256 assetAmount, string memory revertString) internal {
doAction(ActionParameters({
action: Action.Repay,
spokeIndex: spokeIndex,
assetAddress: asset.assetAddress,
assetAmount: assetAmount,
expectRevert: true,
revertString: revertString,
paymentReversion: false,
prank: false,
prankAddress: address(0x0)
}));
}
function doRepayRevertPayment(uint256 spokeIndex, Asset memory asset, uint256 assetAmount) internal {
doAction(ActionParameters({
action: Action.Repay,
spokeIndex: spokeIndex,
assetAddress: asset.assetAddress,
assetAmount: assetAmount,
expectRevert: false,
revertString: "",
paymentReversion: true,
prank: false,
prankAddress: address(0x0)
}));
}
function doRepayRevertPayment(uint256 spokeIndex, Asset memory asset, uint256 assetAmount, address vault) internal {
doAction(ActionParameters({
action: Action.Repay,
spokeIndex: spokeIndex,
assetAddress: asset.assetAddress,
assetAmount: assetAmount,
expectRevert: false,
revertString: "",
paymentReversion: true,
prank: true,
prankAddress: vault
}));
}
function doRepayNative(uint256 spokeIndex, uint256 amount) internal {
doAction(ActionParameters({
action: Action.RepayNative,
spokeIndex: spokeIndex,
assetAddress: address(getHubData().tokenBridgeContract.WETH()),
assetAmount: amount,
expectRevert: false,
revertString: "",
paymentReversion: false,
prank: false,
prankAddress: address(0x0)
}));
}
function doRepayNative(uint256 spokeIndex, uint256 amount, address vault) internal {
doAction(ActionParameters({
action: Action.RepayNative,
spokeIndex: spokeIndex,
assetAddress: address(getHubData().tokenBridgeContract.WETH()),
assetAmount: amount,
expectRevert: false,
revertString: "",
paymentReversion: false,
prank: true,
prankAddress: vault
}));
}
function doRepayNativeRevertPayment(uint256 spokeIndex, uint256 amount) internal {
doAction(ActionParameters({
action: Action.RepayNative,
spokeIndex: spokeIndex,
assetAddress: address(getHubData().tokenBridgeContract.WETH()),
assetAmount: amount,
expectRevert: false,
revertString: "",
paymentReversion: true,
prank: false,
prankAddress: address(0x0)
}));
}
function doRepayNativeRevertPayment(uint256 spokeIndex, uint256 amount, address vault) internal {
doAction(ActionParameters({
action: Action.RepayNative,
spokeIndex: spokeIndex,
assetAddress: address(getHubData().tokenBridgeContract.WETH()),
assetAmount: amount,
expectRevert: false,
revertString: "",
paymentReversion: true,
prank: true,
prankAddress: vault
}));
}
function doBorrow(uint256 spokeIndex, Asset memory asset, uint256 assetAmount) internal {
doAction(ActionParameters({
action: Action.Borrow,
spokeIndex: spokeIndex,
assetAddress: asset.assetAddress,
assetAmount: assetAmount,
expectRevert: false,
revertString: "",
paymentReversion: false,
prank: false,
prankAddress: address(0x0)
}));
}
function doBorrow(uint256 spokeIndex, Asset memory asset, uint256 assetAmount, address vault) internal {
doAction(ActionParameters({
action: Action.Borrow,
spokeIndex: spokeIndex,
assetAddress: asset.assetAddress,
assetAmount: assetAmount,
expectRevert: false,
revertString: "",
paymentReversion: false,
prank: true,
prankAddress: vault
}));
}
function doBorrowRevert(uint256 spokeIndex, Asset memory asset, uint256 assetAmount, string memory revertString) internal {
doAction(ActionParameters({
action: Action.Borrow,
spokeIndex: spokeIndex,
assetAddress: asset.assetAddress,
assetAmount: assetAmount,
expectRevert: true,
revertString: revertString,
paymentReversion: false,
prank: false,
prankAddress: address(0x0)
}));
}
function doBorrowRevert(uint256 spokeIndex, Asset memory asset, uint256 assetAmount, string memory revertString, address prankAddress) internal {
doAction(ActionParameters({
action: Action.Borrow,
spokeIndex: spokeIndex,
assetAddress: asset.assetAddress,
assetAmount: assetAmount,
expectRevert: true,
revertString: revertString,
paymentReversion: false,
prank: true,
prankAddress: prankAddress
}));
}
function doWithdraw(uint256 spokeIndex, Asset memory asset, uint256 assetAmount) internal {
doAction(ActionParameters({
action: Action.Withdraw,
spokeIndex: spokeIndex,
assetAddress: asset.assetAddress,
assetAmount: assetAmount,
expectRevert: false,
revertString: "",
paymentReversion: false,
prank: false,
prankAddress: address(0x0)
}));
}
function doWithdraw(uint256 spokeIndex, Asset memory asset, uint256 assetAmount, address vault) internal {
doAction(ActionParameters({
action: Action.Withdraw,
spokeIndex: spokeIndex,
assetAddress: asset.assetAddress,
assetAmount: assetAmount,
expectRevert: false,
revertString: "",
paymentReversion: false,
prank: true,
prankAddress: vault
}));
}
function doWithdrawRevert(uint256 spokeIndex, Asset memory asset, uint256 assetAmount, string memory revertString) internal {
doAction(ActionParameters({
action: Action.Withdraw,
spokeIndex: spokeIndex,
assetAddress: asset.assetAddress,
assetAmount: assetAmount,
expectRevert: true,
revertString: revertString,
paymentReversion: false,
prank: false,
prankAddress: address(0x0)
}));
}
function doWithdrawRevert(uint256 spokeIndex, Asset memory asset, uint256 assetAmount, address vault, string memory revertString) internal {
doAction(ActionParameters({
action: Action.Withdraw,
spokeIndex: spokeIndex,
assetAddress: asset.assetAddress,
assetAmount: assetAmount,
expectRevert: true,
revertString: revertString,
paymentReversion: false,
prank: true,
prankAddress: vault
}));
}
function setPrice(Asset memory asset, int64 price) internal {
setPrice(asset, price, 0, 0, 100, 100);
}
function setPrice(Asset memory asset, int64 price, uint64 conf, int32 expo, int64 emaPrice, uint64 emaConf) internal {
uint64 publishTime = getPublishTime();
publishTime += 1;
setPublishTime(publishTime);
if(getOracleMode() == 1){
getHub().setMockPythFeed(asset.pythId, price, conf, expo, emaPrice, emaConf, publishTime);
}
else if(getOracleMode() == 2){
getHub().setOraclePrice(asset.pythId, Price({price: price, conf: conf, expo: expo, publishTime: publishTime}));
}
}
function doLiquidate(address vaultToLiquidate, address[] memory repayAddresses, uint256[] memory repayAmounts, address[] memory receiptAddresses, uint256[] memory receiptAmounts) internal {
doLiquidate(vaultToLiquidate, repayAddresses, repayAmounts, receiptAddresses, receiptAmounts, false, "");
}
function doLiquidate(address vaultToLiquidate, address[] memory repayAddresses, uint256[] memory repayAmounts, address[] memory receiptAddresses, uint256[] memory receiptAmounts, string memory revertString) internal {
doLiquidate(vaultToLiquidate, repayAddresses, repayAmounts, receiptAddresses, receiptAmounts, true, revertString);
}
function doLiquidate(address vaultToLiquidate, address[] memory repayAddresses, uint256[] memory repayAmounts, address[] memory receiptAddresses, uint256[] memory receiptAmounts, bool expectRevert, string memory revertString) internal {
uint256 repayLength = repayAddresses.length;
uint256 receiptLength = repayAddresses.length;
LiquidationDataArrays memory lda;
lda.userBalancePreRepay = new uint256[](repayLength);
lda.hubBalancePreRepay = new uint256[](repayLength);
lda.userBalancePostRepay = new uint256[](repayLength);
lda.hubBalancePostRepay = new uint256[](repayLength);
lda.userBalancePreReceipt = new uint256[](receiptLength);
lda.hubBalancePreReceipt = new uint256[](receiptLength);
lda.userBalancePostReceipt = new uint256[](receiptLength);
lda.hubBalancePostReceipt = new uint256[](receiptLength);
lda.vaultToLiquidateAmountRepayPre = new uint256[](repayLength);
lda.vaultToLiquidateAmountReceiptPre = new uint256[](receiptLength);
lda.vaultToLiquidateAmountRepayPost = new uint256[](repayLength);
lda.vaultToLiquidateAmountReceiptPost = new uint256[](receiptLength);
lda.globalAmountRepayPre = new uint256[](repayLength);
lda.globalAmountReceiptPre = new uint256[](receiptLength);
lda.globalAmountRepayPost = new uint256[](repayLength);
lda.globalAmountReceiptPost = new uint256[](receiptLength);
for(uint256 i=0; i<repayLength; i++) {
IERC20(repayAddresses[i]).approve(address(getHub()), repayAmounts[i]);
lda.userBalancePreRepay[i] = IERC20(repayAddresses[i]).balanceOf(address(this));
lda.hubBalancePreRepay[i] = IERC20(repayAddresses[i]).balanceOf(address(getHub()));
lda.vaultToLiquidateAmountRepayPre[i] = getHub().getUserBalance(vaultToLiquidate, repayAddresses[i]).borrowed;
lda.globalAmountRepayPre[i] = getHub().getGlobalBalance(repayAddresses[i]).borrowed;
}
for(uint256 i=0; i<receiptLength; i++) {
lda.userBalancePreReceipt[i] = IERC20(receiptAddresses[i]).balanceOf(address(this));
lda.hubBalancePreReceipt[i] = IERC20(receiptAddresses[i]).balanceOf(address(getHub()));
lda.vaultToLiquidateAmountReceiptPre[i] = getHub().getUserBalance(vaultToLiquidate, receiptAddresses[i]).deposited;
lda.globalAmountReceiptPre[i] = getHub().getGlobalBalance(receiptAddresses[i]).deposited;
}
if(expectRevert) {
getVm().expectRevert(bytes(revertString));
}
getHub().liquidation(vaultToLiquidate, repayAddresses, repayAmounts, receiptAddresses, receiptAmounts);
if(expectRevert) {
return;
}
for(uint256 i=0; i<repayLength; i++) {
lda.userBalancePostRepay[i] = IERC20(repayAddresses[i]).balanceOf(address(this));
lda.hubBalancePostRepay[i] = IERC20(repayAddresses[i]).balanceOf(address(getHub()));
lda.vaultToLiquidateAmountRepayPost[i] = getHub().getUserBalance(vaultToLiquidate, repayAddresses[i]).borrowed;
lda.globalAmountRepayPost[i] = getHub().getGlobalBalance(repayAddresses[i]).borrowed;
require(lda.userBalancePreRepay[i] == lda.userBalancePostRepay[i] + repayAmounts[i], "User didn't pay tokens for the repay");
require(lda.hubBalancePreRepay[i] + repayAmounts[i] == lda.hubBalancePostRepay[i], "Hub didn't receive tokens for the repay");
require(lda.vaultToLiquidateAmountRepayPost[i] + repayAmounts[i]== lda.vaultToLiquidateAmountRepayPre[i], "Vault repay amount not tracked properly");
require(lda.globalAmountRepayPost[i] + repayAmounts[i] == lda.globalAmountRepayPre[i], "Global repay amount not tracked properly");
}
for(uint256 i=0; i<receiptLength; i++) {
lda.userBalancePostReceipt[i] = IERC20(receiptAddresses[i]).balanceOf(address(this));
lda.hubBalancePostReceipt[i] = IERC20(receiptAddresses[i]).balanceOf(address(getHub()));
lda.vaultToLiquidateAmountReceiptPost[i] = getHub().getUserBalance(vaultToLiquidate, receiptAddresses[i]).deposited;
lda.globalAmountReceiptPost[i] = getHub().getGlobalBalance(receiptAddresses[i]).deposited;
require(lda.userBalancePreReceipt[i] + receiptAmounts[i] == lda.userBalancePostReceipt[i], "User didn't receive tokens for the receipt");
require(lda.hubBalancePreReceipt[i] == lda.hubBalancePostReceipt[i] + receiptAmounts[i], "Hub didn't pay tokens for the receipt");
require(lda.vaultToLiquidateAmountReceiptPost[i] + receiptAmounts[i] == lda.vaultToLiquidateAmountReceiptPre[i], "Vault receipt amount not tracked properly");
require(lda.globalAmountReceiptPost[i] + receiptAmounts[i] == lda.globalAmountReceiptPre[i] , "Global receipt amount not tracked properly");
}
}
}