Add CrossChainBorrowLend (#1)
Co-authored-by: Reptile <43194093+dsterioti@users.noreply.github.com>
This commit is contained in:
parent
f835511fbc
commit
c8fac88a0a
|
@ -0,0 +1,6 @@
|
|||
cache
|
||||
lib
|
||||
node_modules
|
||||
out
|
||||
venus-protocol
|
||||
wormhole
|
|
@ -0,0 +1,25 @@
|
|||
.PHONY: dependencies wormhole_dependencies venus_dependencies
|
||||
|
||||
all: build
|
||||
|
||||
.PHONY: dependencies
|
||||
dependencies: forge_dependencies venus_dependencies wormhole_dependencies
|
||||
|
||||
.PHONY: forge_dependencies
|
||||
forge_dependencies: lib/forge-std
|
||||
|
||||
lib/forge-std:
|
||||
forge install foundry-rs/forge-std --no-git
|
||||
|
||||
.PHONY: venus_dependencies
|
||||
venus_dependencies: venus-protocol
|
||||
|
||||
venus-protocol:
|
||||
git clone --depth 1 --branch develop --single-branch https://github.com/VenusProtocol/venus-protocol
|
||||
|
||||
.PHONY: wormhole_dependencies
|
||||
wormhole_dependencies: wormhole/ethereum/build
|
||||
|
||||
wormhole/ethereum/build:
|
||||
git clone --depth 1 --branch dev.v2 --single-branch https://github.com/certusone/wormhole.git
|
||||
# cd wormhole/ethereum && npm ci && npm run build && make .env
|
|
@ -0,0 +1,11 @@
|
|||
[profile.default]
|
||||
src = 'src'
|
||||
out = 'out'
|
||||
libs = ['lib']
|
||||
|
||||
# See more config options https://github.com/foundry-rs/foundry/tree/master/config
|
||||
|
||||
|
||||
remappings = [
|
||||
"@openzeppelin/=node_modules/@openzeppelin/",
|
||||
]
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "wormhole-lending-example-1-evm",
|
||||
"version": "0.0.1",
|
||||
"description": "EVM Contracts for Wormhole Lending Example 1",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"lib": "lib",
|
||||
"test": "test"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"chai": "^4.3.6",
|
||||
"ethers": "^5.7.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openzeppelin/contracts": "^4.7.3",
|
||||
"@types/chai": "^4.3.3",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"elliptic": "^6.5.4",
|
||||
"mocha": "^10.0.0",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"typescript": "^4.8.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,635 @@
|
|||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
||||
|
||||
import "./interfaces/IWormhole.sol";
|
||||
import "./libraries/external/BytesLib.sol";
|
||||
|
||||
import "./CrossChainBorrowLendStructs.sol";
|
||||
import "./CrossChainBorrowLendGetters.sol";
|
||||
import "./CrossChainBorrowLendMessages.sol";
|
||||
|
||||
contract CrossChainBorrowLend is
|
||||
CrossChainBorrowLendGetters,
|
||||
CrossChainBorrowLendMessages,
|
||||
ReentrancyGuard
|
||||
{
|
||||
constructor(
|
||||
address wormholeContractAddress_,
|
||||
uint8 consistencyLevel_,
|
||||
address mockPythAddress_,
|
||||
uint16 targetChainId_,
|
||||
bytes32 targetContractAddress_,
|
||||
address collateralAsset_,
|
||||
bytes32 collateralAssetPythId_,
|
||||
uint256 collateralizationRatio_,
|
||||
address borrowingAsset_,
|
||||
bytes32 borrowingAssetPythId_,
|
||||
uint256 repayGracePeriod_
|
||||
) {
|
||||
// REVIEW: set owner for only owner methods if desired
|
||||
|
||||
// wormhole
|
||||
state.wormholeContractAddress = wormholeContractAddress_;
|
||||
state.consistencyLevel = consistencyLevel_;
|
||||
|
||||
// target chain info
|
||||
state.targetChainId = targetChainId_;
|
||||
state.targetContractAddress = targetContractAddress_;
|
||||
|
||||
// collateral params
|
||||
state.collateralAssetAddress = collateralAsset_;
|
||||
state.collateralizationRatio = collateralizationRatio_;
|
||||
state.collateralizationRatioPrecision = 1e18; // fixed
|
||||
|
||||
// borrowing asset address
|
||||
state.borrowingAssetAddress = borrowingAsset_;
|
||||
|
||||
// interest rate parameters
|
||||
state.interestRateModel.ratePrecision = 1e18;
|
||||
state.interestRateModel.rateIntercept = 2e16; // 2%
|
||||
state.interestRateModel.rateCoefficientA = 0;
|
||||
|
||||
// Price index of 1 with the current precision is 1e18
|
||||
// since this is the precision of our value.
|
||||
state.interestAccrualIndexPrecision = 1e18;
|
||||
state.interestAccrualIndex = state.interestAccrualIndexPrecision;
|
||||
|
||||
// pyth oracle address and asset IDs
|
||||
state.mockPythAddress = mockPythAddress_;
|
||||
state.collateralAssetPythId = collateralAssetPythId_;
|
||||
state.borrowingAssetPythId = borrowingAssetPythId_;
|
||||
|
||||
// repay grace period for this chain
|
||||
state.repayGracePeriod = repayGracePeriod_;
|
||||
}
|
||||
|
||||
function addCollateral(uint256 amount) public nonReentrant {
|
||||
require(amount > 0, "nothing to deposit");
|
||||
|
||||
// update current price index
|
||||
updateInterestAccrualIndex();
|
||||
|
||||
// update state for supplier
|
||||
uint256 normalizedAmount = normalizeAmount(
|
||||
amount,
|
||||
collateralInterestAccrualIndex()
|
||||
);
|
||||
state.accountAssets[_msgSender()].sourceDeposited += normalizedAmount;
|
||||
state.totalAssets.deposited += normalizedAmount;
|
||||
|
||||
SafeERC20.safeTransferFrom(
|
||||
collateralToken(),
|
||||
_msgSender(),
|
||||
address(this),
|
||||
amount
|
||||
);
|
||||
}
|
||||
|
||||
function removeCollateral(uint256 amount) public nonReentrant {
|
||||
require(amount > 0, "nothing to withdraw");
|
||||
|
||||
// update current price index
|
||||
updateInterestAccrualIndex();
|
||||
|
||||
// Check if user has enough to withdraw from the contract
|
||||
require(
|
||||
amount < maxAllowedToWithdraw(_msgSender()),
|
||||
"amount >= maxAllowedToWithdraw(msg.sender)"
|
||||
);
|
||||
|
||||
// update state for supplier
|
||||
uint256 normalizedAmount = normalizeAmount(
|
||||
amount,
|
||||
collateralInterestAccrualIndex()
|
||||
);
|
||||
state.accountAssets[_msgSender()].sourceDeposited -= normalizedAmount;
|
||||
state.totalAssets.deposited -= normalizedAmount;
|
||||
|
||||
// transfer the tokens to the caller
|
||||
SafeERC20.safeTransfer(collateralToken(), _msgSender(), amount);
|
||||
}
|
||||
|
||||
function removeCollateralInFull() public nonReentrant {
|
||||
// fetch the account information for the caller
|
||||
NormalizedAmounts memory normalizedAmounts = state.accountAssets[
|
||||
_msgSender()
|
||||
];
|
||||
|
||||
// make sure the account has closed all borrowed positions
|
||||
require(
|
||||
normalizedAmounts.targetBorrowed == 0,
|
||||
"account has outstanding loans"
|
||||
);
|
||||
|
||||
// update current price index
|
||||
updateInterestAccrualIndex();
|
||||
|
||||
// update state for supplier
|
||||
uint256 normalizedAmount = normalizedAmounts.sourceDeposited;
|
||||
state.accountAssets[_msgSender()].sourceDeposited = 0;
|
||||
state.totalAssets.deposited -= normalizedAmount;
|
||||
|
||||
// transfer the tokens to the caller
|
||||
SafeERC20.safeTransfer(
|
||||
collateralToken(),
|
||||
_msgSender(),
|
||||
denormalizeAmount(
|
||||
normalizedAmount,
|
||||
collateralInterestAccrualIndex()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function computeInterestProportion(
|
||||
uint256 secondsElapsed,
|
||||
uint256 intercept,
|
||||
uint256 coefficient
|
||||
) internal view returns (uint256) {
|
||||
uint256 deposited = state.totalAssets.deposited;
|
||||
if (deposited == 0) {
|
||||
return 0;
|
||||
}
|
||||
return
|
||||
(secondsElapsed *
|
||||
(intercept +
|
||||
(coefficient * state.totalAssets.borrowed) /
|
||||
deposited)) /
|
||||
365 /
|
||||
24 /
|
||||
60 /
|
||||
60;
|
||||
}
|
||||
|
||||
function updateInterestAccrualIndex() internal {
|
||||
// TODO: change to block.number?
|
||||
uint256 secondsElapsed = block.timestamp -
|
||||
state.lastActivityBlockTimestamp;
|
||||
|
||||
if (secondsElapsed == 0) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
// Should not hit, but just here in case someone
|
||||
// tries to update the interest when there is nothing
|
||||
// deposited.
|
||||
uint256 deposited = state.totalAssets.deposited;
|
||||
if (deposited == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.lastActivityBlockTimestamp = block.timestamp;
|
||||
|
||||
state.interestAccrualIndex += computeInterestProportion(
|
||||
secondsElapsed,
|
||||
state.interestRateModel.rateIntercept,
|
||||
state.interestRateModel.rateCoefficientA
|
||||
);
|
||||
}
|
||||
|
||||
function initiateBorrow(uint256 amount) public returns (uint64 sequence) {
|
||||
require(amount > 0, "nothing to borrow");
|
||||
|
||||
// update current price index
|
||||
updateInterestAccrualIndex();
|
||||
|
||||
// Check if user has enough to borrow
|
||||
require(
|
||||
amount < maxAllowedToBorrow(_msgSender()),
|
||||
"amount >= maxAllowedToBorrow(msg.sender)"
|
||||
);
|
||||
|
||||
// update state for borrower
|
||||
uint256 borrowedIndex = borrowedInterestAccrualIndex();
|
||||
uint256 normalizedAmount = normalizeAmount(amount, borrowedIndex);
|
||||
state.accountAssets[_msgSender()].targetBorrowed += normalizedAmount;
|
||||
state.totalAssets.borrowed += normalizedAmount;
|
||||
|
||||
// construct wormhole message
|
||||
MessageHeader memory header = MessageHeader({
|
||||
payloadID: uint8(1),
|
||||
borrower: _msgSender(),
|
||||
collateralAddress: state.collateralAssetAddress,
|
||||
borrowAddress: state.borrowingAssetAddress
|
||||
});
|
||||
|
||||
sequence = sendWormholeMessage(
|
||||
encodeBorrowMessage(
|
||||
BorrowMessage({
|
||||
header: header,
|
||||
borrowAmount: amount,
|
||||
totalNormalizedBorrowAmount: state
|
||||
.accountAssets[_msgSender()]
|
||||
.targetBorrowed,
|
||||
interestAccrualIndex: borrowedIndex
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function completeBorrow(bytes calldata encodedVm)
|
||||
public
|
||||
returns (uint64 sequence)
|
||||
{
|
||||
// parse and verify the wormhole BorrowMessage
|
||||
(
|
||||
IWormhole.VM memory parsed,
|
||||
bool valid,
|
||||
string memory reason
|
||||
) = wormhole().parseAndVerifyVM(encodedVm);
|
||||
require(valid, reason);
|
||||
|
||||
// verify emitter
|
||||
require(verifyEmitter(parsed), "invalid emitter");
|
||||
|
||||
// completed (replay protection)
|
||||
// also serves as reentrancy protection
|
||||
require(!messageHashConsumed(parsed.hash), "message already consumed");
|
||||
consumeMessageHash(parsed.hash);
|
||||
|
||||
// decode borrow message
|
||||
BorrowMessage memory params = decodeBorrowMessage(parsed.payload);
|
||||
|
||||
// correct assets?
|
||||
require(verifyAssetMetaFromBorrow(params), "invalid asset metadata");
|
||||
|
||||
// make sure this contract has enough assets to fund the borrow
|
||||
if (
|
||||
params.borrowAmount >
|
||||
denormalizeAmount(
|
||||
normalizedLiquidity(),
|
||||
borrowedInterestAccrualIndex()
|
||||
)
|
||||
) {
|
||||
// construct RevertBorrow wormhole message
|
||||
// switch the borrow and collateral addresses for the target chain
|
||||
MessageHeader memory header = MessageHeader({
|
||||
payloadID: uint8(2),
|
||||
borrower: params.header.borrower,
|
||||
collateralAddress: state.borrowingAssetAddress,
|
||||
borrowAddress: state.collateralAssetAddress
|
||||
});
|
||||
|
||||
sequence = sendWormholeMessage(
|
||||
encodeRevertBorrowMessage(
|
||||
RevertBorrowMessage({
|
||||
header: header,
|
||||
borrowAmount: params.borrowAmount,
|
||||
sourceInterestAccrualIndex: params.interestAccrualIndex
|
||||
})
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// save the total normalized borrow amount for repayments
|
||||
state.totalAssets.borrowed +=
|
||||
params.totalNormalizedBorrowAmount -
|
||||
state.accountAssets[params.header.borrower].sourceBorrowed;
|
||||
state.accountAssets[params.header.borrower].sourceBorrowed = params
|
||||
.totalNormalizedBorrowAmount;
|
||||
|
||||
// params.borrowAmount == 0 means that there was a repayment
|
||||
// made outside of the grace period, so we will have received
|
||||
// another VAA representing the updated borrowed amount
|
||||
// on the source chain.
|
||||
if (params.borrowAmount > 0) {
|
||||
// finally transfer
|
||||
SafeERC20.safeTransferFrom(
|
||||
collateralToken(),
|
||||
address(this),
|
||||
params.header.borrower,
|
||||
params.borrowAmount
|
||||
);
|
||||
}
|
||||
|
||||
// no wormhole message, return the default value: zero == success
|
||||
}
|
||||
}
|
||||
|
||||
function completeRevertBorrow(bytes calldata encodedVm) public {
|
||||
// parse and verify the wormhole RevertBorrowMessage
|
||||
(
|
||||
IWormhole.VM memory parsed,
|
||||
bool valid,
|
||||
string memory reason
|
||||
) = wormhole().parseAndVerifyVM(encodedVm);
|
||||
require(valid, reason);
|
||||
|
||||
// verify emitter
|
||||
require(verifyEmitter(parsed), "invalid emitter");
|
||||
|
||||
// completed (replay protection)
|
||||
// also serves as reentrancy protection
|
||||
require(!messageHashConsumed(parsed.hash), "message already consumed");
|
||||
consumeMessageHash(parsed.hash);
|
||||
|
||||
// decode borrow message
|
||||
RevertBorrowMessage memory params = decodeRevertBorrowMessage(
|
||||
parsed.payload
|
||||
);
|
||||
|
||||
// verify asset meta
|
||||
require(
|
||||
state.collateralAssetAddress == params.header.collateralAddress &&
|
||||
state.borrowingAssetAddress == params.header.borrowAddress,
|
||||
"invalid asset metadata"
|
||||
);
|
||||
|
||||
// update state for borrower
|
||||
// Normalize the borrowAmount by the original interestAccrualIndex (encoded in the BorrowMessage)
|
||||
// to revert the inteded borrow amount.
|
||||
uint256 normalizedAmount = normalizeAmount(
|
||||
params.borrowAmount,
|
||||
params.sourceInterestAccrualIndex
|
||||
);
|
||||
state
|
||||
.accountAssets[params.header.borrower]
|
||||
.targetBorrowed -= normalizedAmount;
|
||||
state.totalAssets.borrowed -= normalizedAmount;
|
||||
}
|
||||
|
||||
function initiateRepay(uint256 amount)
|
||||
public
|
||||
nonReentrant
|
||||
returns (uint64 sequence)
|
||||
{
|
||||
require(amount > 0, "nothing to repay");
|
||||
|
||||
// For EVMs, same private key will be used for borrowing-lending activity.
|
||||
// When introducing other chains (e.g. Cosmos), need to do wallet registration
|
||||
// so we can access a map of a non-EVM address based on this EVM borrower
|
||||
NormalizedAmounts memory normalizedAmounts = state.accountAssets[
|
||||
_msgSender()
|
||||
];
|
||||
|
||||
// update the index
|
||||
updateInterestAccrualIndex();
|
||||
|
||||
// cache the index to save gas
|
||||
uint256 index = borrowedInterestAccrualIndex();
|
||||
|
||||
// save the normalized amount
|
||||
uint256 normalizedAmount = normalizeAmount(amount, index);
|
||||
|
||||
// confirm that the caller has loans to pay back
|
||||
require(
|
||||
normalizedAmount <= normalizedAmounts.sourceBorrowed,
|
||||
"loan payment too large"
|
||||
);
|
||||
|
||||
// update state on this contract
|
||||
state.accountAssets[_msgSender()].sourceBorrowed -= normalizedAmount;
|
||||
state.totalAssets.borrowed -= normalizedAmount;
|
||||
|
||||
// transfer to this contract
|
||||
SafeERC20.safeTransferFrom(
|
||||
borrowToken(),
|
||||
_msgSender(),
|
||||
address(this),
|
||||
amount
|
||||
);
|
||||
|
||||
// construct wormhole message
|
||||
MessageHeader memory header = MessageHeader({
|
||||
payloadID: uint8(3),
|
||||
borrower: _msgSender(),
|
||||
collateralAddress: state.borrowingAssetAddress,
|
||||
borrowAddress: state.collateralAssetAddress
|
||||
});
|
||||
|
||||
// add index and block timestamp
|
||||
sequence = sendWormholeMessage(
|
||||
encodeRepayMessage(
|
||||
RepayMessage({
|
||||
header: header,
|
||||
repayAmount: amount,
|
||||
targetInterestAccrualIndex: index,
|
||||
repayTimestamp: block.timestamp,
|
||||
paidInFull: 0
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function initiateRepayInFull()
|
||||
public
|
||||
nonReentrant
|
||||
returns (uint64 sequence)
|
||||
{
|
||||
// For EVMs, same private key will be used for borrowing-lending activity.
|
||||
// When introducing other chains (e.g. Cosmos), need to do wallet registration
|
||||
// so we can access a map of a non-EVM address based on this EVM borrower
|
||||
NormalizedAmounts memory normalizedAmounts = state.accountAssets[
|
||||
_msgSender()
|
||||
];
|
||||
|
||||
// update the index
|
||||
updateInterestAccrualIndex();
|
||||
|
||||
// cache the index to save gas
|
||||
uint256 index = borrowedInterestAccrualIndex();
|
||||
|
||||
// update state on the contract
|
||||
uint256 normalizedAmount = normalizedAmounts.sourceBorrowed;
|
||||
state.accountAssets[_msgSender()].sourceBorrowed = 0;
|
||||
state.totalAssets.borrowed -= normalizedAmount;
|
||||
|
||||
// transfer to this contract
|
||||
SafeERC20.safeTransferFrom(
|
||||
borrowToken(),
|
||||
_msgSender(),
|
||||
address(this),
|
||||
denormalizeAmount(normalizedAmount, index)
|
||||
);
|
||||
|
||||
// construct wormhole message
|
||||
MessageHeader memory header = MessageHeader({
|
||||
payloadID: uint8(3),
|
||||
borrower: _msgSender(),
|
||||
collateralAddress: state.borrowingAssetAddress,
|
||||
borrowAddress: state.collateralAssetAddress
|
||||
});
|
||||
|
||||
// add index and block timestamp
|
||||
sequence = sendWormholeMessage(
|
||||
encodeRepayMessage(
|
||||
RepayMessage({
|
||||
header: header,
|
||||
repayAmount: denormalizeAmount(normalizedAmount, index),
|
||||
targetInterestAccrualIndex: index,
|
||||
repayTimestamp: block.timestamp,
|
||||
paidInFull: 1
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function completeRepay(bytes calldata encodedVm)
|
||||
public
|
||||
returns (uint64 sequence)
|
||||
{
|
||||
// parse and verify the RepayMessage
|
||||
(
|
||||
IWormhole.VM memory parsed,
|
||||
bool valid,
|
||||
string memory reason
|
||||
) = wormhole().parseAndVerifyVM(encodedVm);
|
||||
require(valid, reason);
|
||||
|
||||
// verify emitter
|
||||
require(verifyEmitter(parsed), "invalid emitter");
|
||||
|
||||
// completed (replay protection)
|
||||
require(!messageHashConsumed(parsed.hash), "message already consumed");
|
||||
consumeMessageHash(parsed.hash);
|
||||
|
||||
// update the index
|
||||
updateInterestAccrualIndex();
|
||||
|
||||
// cache the index to save gas
|
||||
uint256 index = borrowedInterestAccrualIndex();
|
||||
|
||||
// decode the RepayMessage
|
||||
RepayMessage memory params = decodeRepayMessage(parsed.payload);
|
||||
|
||||
// correct assets?
|
||||
require(verifyAssetMetaFromRepay(params), "invalid asset metadata");
|
||||
|
||||
// see if the loan is repaid in full
|
||||
if (params.paidInFull == 1) {
|
||||
// REVIEW: do we care about getting the VAA in time?
|
||||
if (
|
||||
params.repayTimestamp + state.repayGracePeriod <=
|
||||
block.timestamp
|
||||
) {
|
||||
// update state in this contract
|
||||
uint256 normalizedAmount = normalizeAmount(
|
||||
params.repayAmount,
|
||||
params.targetInterestAccrualIndex
|
||||
);
|
||||
state.accountAssets[params.header.borrower].targetBorrowed = 0;
|
||||
state.totalAssets.borrowed -= normalizedAmount;
|
||||
} else {
|
||||
uint256 normalizedAmount = normalizeAmount(
|
||||
params.repayAmount,
|
||||
index
|
||||
);
|
||||
state
|
||||
.accountAssets[params.header.borrower]
|
||||
.targetBorrowed -= normalizedAmount;
|
||||
state.totalAssets.borrowed -= normalizedAmount;
|
||||
|
||||
// Send a wormhole message again since he did not repay in full
|
||||
// (due to repaying outside of the grace period)
|
||||
sequence = sendWormholeMessage(
|
||||
encodeBorrowMessage(
|
||||
BorrowMessage({
|
||||
header: MessageHeader({
|
||||
payloadID: uint8(1),
|
||||
borrower: params.header.borrower,
|
||||
collateralAddress: state.collateralAssetAddress,
|
||||
borrowAddress: state.borrowingAssetAddress
|
||||
}),
|
||||
borrowAmount: 0, // special value to indicate failed repay in full
|
||||
totalNormalizedBorrowAmount: state
|
||||
.accountAssets[params.header.borrower]
|
||||
.targetBorrowed,
|
||||
interestAccrualIndex: index
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// update state in this contract
|
||||
uint256 normalizedAmount = normalizeAmount(
|
||||
params.repayAmount,
|
||||
params.targetInterestAccrualIndex
|
||||
);
|
||||
state
|
||||
.accountAssets[params.header.borrower]
|
||||
.targetBorrowed -= normalizedAmount;
|
||||
state.totalAssets.borrowed -= normalizedAmount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@notice `initiateLiquidationOnTargetChain` has not been implemented yet.
|
||||
|
||||
This function should determine if a particular position is undercollateralized
|
||||
by querying the `accountAssets` state variable for the passed account. Calculate
|
||||
the health of the account.
|
||||
|
||||
If an account is undercollateralized, this method should generate a Wormhole
|
||||
message sent to the target chain by the caller. The caller will invoke the
|
||||
`completeRepayOnBehalf` method on the target chain and pass the signed Wormhole
|
||||
message as an argument.
|
||||
|
||||
If the account has not yet paid the loan back by the time the Wormhole message
|
||||
arrives on the target chain, `completeRepayOnBehalf` will accept funds from the
|
||||
caller, and generate another Wormhole messsage to be delivered to the source chain.
|
||||
|
||||
The caller will then invoke `completeLiquidation` on the source chain and pass
|
||||
the signed Wormhole message in as an argument. This function should handle
|
||||
releasing the account's collateral to the liquidator, less fees (which should be
|
||||
defined in the contract and updated by the contract owner).
|
||||
|
||||
In order for off-chain processes to calculate an account's health, the integrator
|
||||
needs to expose a getter that will return the list of accounts with open positions.
|
||||
The integrator needs to expose a getter that allows the liquidator to query the
|
||||
`accountAssets` state variable for a particular account.
|
||||
*/
|
||||
function initiateLiquidationOnTargetChain(address accountToLiquidate)
|
||||
public
|
||||
{}
|
||||
|
||||
function completeRepayOnBehalf(bytes calldata encodedVm) public {}
|
||||
|
||||
function completeLiquidation(bytes calldata encodedVm) public {}
|
||||
|
||||
function sendWormholeMessage(bytes memory payload)
|
||||
internal
|
||||
returns (uint64 sequence)
|
||||
{
|
||||
sequence = IWormhole(state.wormholeContractAddress).publishMessage(
|
||||
0, // nonce
|
||||
payload,
|
||||
state.consistencyLevel
|
||||
);
|
||||
}
|
||||
|
||||
function verifyEmitter(IWormhole.VM memory parsed)
|
||||
internal
|
||||
view
|
||||
returns (bool)
|
||||
{
|
||||
return
|
||||
parsed.emitterAddress == state.targetContractAddress &&
|
||||
parsed.emitterChainId == state.targetChainId;
|
||||
}
|
||||
|
||||
function verifyAssetMetaFromBorrow(BorrowMessage memory params)
|
||||
internal
|
||||
view
|
||||
returns (bool)
|
||||
{
|
||||
return
|
||||
params.header.collateralAddress == state.borrowingAssetAddress &&
|
||||
params.header.borrowAddress == state.collateralAssetAddress;
|
||||
}
|
||||
|
||||
function verifyAssetMetaFromRepay(RepayMessage memory params)
|
||||
internal
|
||||
view
|
||||
returns (bool)
|
||||
{
|
||||
return
|
||||
params.header.collateralAddress == state.collateralAssetAddress &&
|
||||
params.header.borrowAddress == state.borrowingAssetAddress;
|
||||
}
|
||||
|
||||
function consumeMessageHash(bytes32 vmHash) internal {
|
||||
state.consumedMessages[vmHash] = true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
// SPDX-License-Identifier: UNLICENSED
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
|
||||
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
|
||||
|
||||
import "./interfaces/IMockPyth.sol";
|
||||
import "./interfaces/IWormhole.sol";
|
||||
import "./CrossChainBorrowLendState.sol";
|
||||
|
||||
contract CrossChainBorrowLendGetters is Context, CrossChainBorrowLendState {
|
||||
function wormhole() internal view returns (IWormhole) {
|
||||
return IWormhole(state.wormholeContractAddress);
|
||||
}
|
||||
|
||||
function collateralToken() internal view returns (IERC20) {
|
||||
return IERC20(state.collateralAssetAddress);
|
||||
}
|
||||
|
||||
function collateralTokenDecimals() internal view returns (uint8) {
|
||||
return IERC20Metadata(state.collateralAssetAddress).decimals();
|
||||
}
|
||||
|
||||
function borrowToken() internal view returns (IERC20) {
|
||||
return IERC20(state.borrowingAssetAddress);
|
||||
}
|
||||
|
||||
function borrowTokenDecimals() internal view returns (uint8) {
|
||||
return IERC20Metadata(state.borrowingAssetAddress).decimals();
|
||||
}
|
||||
|
||||
function getOraclePrices() internal view returns (uint64, uint64) {
|
||||
IMockPyth.PriceFeed memory collateralFeed = mockPyth().queryPriceFeed(
|
||||
state.collateralAssetPythId
|
||||
);
|
||||
IMockPyth.PriceFeed memory borrowFeed = mockPyth().queryPriceFeed(
|
||||
state.borrowingAssetPythId
|
||||
);
|
||||
|
||||
// sanity check the price feeds
|
||||
require(
|
||||
collateralFeed.price.price > 0 && borrowFeed.price.price > 0,
|
||||
"negative prices detected"
|
||||
);
|
||||
|
||||
// Users of Pyth prices should read: https://docs.pyth.network/consumers/best-practices
|
||||
// before using the price feed. Blindly using the price alone is not recommended.
|
||||
return (
|
||||
uint64(collateralFeed.price.price),
|
||||
uint64(borrowFeed.price.price)
|
||||
);
|
||||
}
|
||||
|
||||
function collateralInterestAccrualIndex() public view returns (uint256) {
|
||||
uint256 deposited = state.totalAssets.deposited;
|
||||
uint256 precision = state.interestAccrualIndexPrecision;
|
||||
if (deposited == 0) {
|
||||
return precision;
|
||||
}
|
||||
return
|
||||
precision +
|
||||
(state.totalAssets.borrowed *
|
||||
(state.interestAccrualIndex - precision)) /
|
||||
deposited;
|
||||
}
|
||||
|
||||
function borrowedInterestAccrualIndex() public view returns (uint256) {
|
||||
return state.interestAccrualIndex;
|
||||
}
|
||||
|
||||
function mockPyth() internal view returns (IMockPyth) {
|
||||
return IMockPyth(state.mockPythAddress);
|
||||
}
|
||||
|
||||
function normalizedLiquidity() internal view returns (uint256) {
|
||||
return state.totalAssets.deposited - state.totalAssets.borrowed;
|
||||
}
|
||||
|
||||
function denormalizeAmount(
|
||||
uint256 normalizedAmount,
|
||||
uint256 interestAccrualIndex_
|
||||
) public view returns (uint256) {
|
||||
return
|
||||
(normalizedAmount * interestAccrualIndex_) /
|
||||
state.interestAccrualIndexPrecision;
|
||||
}
|
||||
|
||||
function normalizeAmount(
|
||||
uint256 denormalizedAmount,
|
||||
uint256 interestAccrualIndex_
|
||||
) public view returns (uint256) {
|
||||
return
|
||||
(denormalizedAmount * state.interestAccrualIndexPrecision) /
|
||||
interestAccrualIndex_;
|
||||
}
|
||||
|
||||
function messageHashConsumed(bytes32 hash) public view returns (bool) {
|
||||
return state.consumedMessages[hash];
|
||||
}
|
||||
|
||||
function normalizedAmounts()
|
||||
public
|
||||
view
|
||||
returns (NormalizedTotalAmounts memory)
|
||||
{
|
||||
return state.totalAssets;
|
||||
}
|
||||
|
||||
function maxAllowedToBorrowWithPrices(
|
||||
address account,
|
||||
uint64 collateralPrice,
|
||||
uint64 borrowAssetPrice
|
||||
) internal view returns (uint256) {
|
||||
// For EVMs, same private key will be used for borrowing-lending activity.
|
||||
// When introducing other chains (e.g. Cosmos), need to do wallet registration
|
||||
// so we can access a map of a non-EVM address based on this EVM borrower
|
||||
NormalizedAmounts memory normalized = state.accountAssets[account];
|
||||
|
||||
// denormalize
|
||||
uint256 denormalizedDeposited = denormalizeAmount(
|
||||
normalized.sourceDeposited,
|
||||
collateralInterestAccrualIndex()
|
||||
);
|
||||
uint256 denormalizedBorrowed = denormalizeAmount(
|
||||
normalized.targetBorrowed,
|
||||
borrowedInterestAccrualIndex()
|
||||
);
|
||||
|
||||
return
|
||||
(denormalizedDeposited *
|
||||
state.collateralizationRatio *
|
||||
collateralPrice *
|
||||
10**borrowTokenDecimals()) /
|
||||
(state.collateralizationRatioPrecision *
|
||||
borrowAssetPrice *
|
||||
10**collateralTokenDecimals()) -
|
||||
denormalizedBorrowed;
|
||||
}
|
||||
|
||||
function maxAllowedToBorrow(address account) public view returns (uint256) {
|
||||
// fetch asset prices
|
||||
(uint64 collateralPrice, uint64 borrowAssetPrice) = getOraclePrices();
|
||||
return
|
||||
maxAllowedToBorrowWithPrices(
|
||||
account,
|
||||
collateralPrice,
|
||||
borrowAssetPrice
|
||||
);
|
||||
}
|
||||
|
||||
function maxAllowedToWithdrawWithPrices(
|
||||
address account,
|
||||
uint64 collateralPrice,
|
||||
uint64 borrowAssetPrice
|
||||
) internal view returns (uint256) {
|
||||
// For EVMs, same private key will be used for borrowing-lending activity.
|
||||
// When introducing other chains (e.g. Cosmos), need to do wallet registration
|
||||
// so we can access a map of a non-EVM address based on this EVM borrower
|
||||
NormalizedAmounts memory normalized = state.accountAssets[account];
|
||||
|
||||
// denormalize
|
||||
uint256 denormalizedDeposited = denormalizeAmount(
|
||||
normalized.sourceDeposited,
|
||||
collateralInterestAccrualIndex()
|
||||
);
|
||||
uint256 denormalizedBorrowed = denormalizeAmount(
|
||||
normalized.targetBorrowed,
|
||||
borrowedInterestAccrualIndex()
|
||||
);
|
||||
|
||||
return
|
||||
denormalizedDeposited -
|
||||
(denormalizedBorrowed *
|
||||
state.collateralizationRatioPrecision *
|
||||
borrowAssetPrice *
|
||||
10**collateralTokenDecimals()) /
|
||||
(state.collateralizationRatio *
|
||||
collateralPrice *
|
||||
10**borrowTokenDecimals());
|
||||
}
|
||||
|
||||
function maxAllowedToWithdraw(address account)
|
||||
public
|
||||
view
|
||||
returns (uint256)
|
||||
{
|
||||
(uint64 collateralPrice, uint64 borrowAssetPrice) = getOraclePrices();
|
||||
return
|
||||
maxAllowedToWithdrawWithPrices(
|
||||
account,
|
||||
collateralPrice,
|
||||
borrowAssetPrice
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
// SPDX-License-Identifier: UNLICENSED
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "./libraries/external/BytesLib.sol";
|
||||
|
||||
import "./CrossChainBorrowLendStructs.sol";
|
||||
|
||||
contract CrossChainBorrowLendMessages {
|
||||
using BytesLib for bytes;
|
||||
|
||||
function encodeMessageHeader(MessageHeader memory header)
|
||||
internal
|
||||
pure
|
||||
returns (bytes memory)
|
||||
{
|
||||
return
|
||||
abi.encodePacked(
|
||||
header.borrower,
|
||||
header.collateralAddress,
|
||||
header.borrowAddress
|
||||
);
|
||||
}
|
||||
|
||||
function encodeBorrowMessage(BorrowMessage memory message)
|
||||
internal
|
||||
pure
|
||||
returns (bytes memory)
|
||||
{
|
||||
return
|
||||
abi.encodePacked(
|
||||
uint8(1), // payloadID
|
||||
encodeMessageHeader(message.header),
|
||||
message.borrowAmount,
|
||||
message.totalNormalizedBorrowAmount,
|
||||
message.interestAccrualIndex
|
||||
);
|
||||
}
|
||||
|
||||
function encodeRevertBorrowMessage(RevertBorrowMessage memory message)
|
||||
internal
|
||||
pure
|
||||
returns (bytes memory)
|
||||
{
|
||||
return
|
||||
abi.encodePacked(
|
||||
uint8(2), // payloadID
|
||||
encodeMessageHeader(message.header),
|
||||
message.borrowAmount,
|
||||
message.sourceInterestAccrualIndex
|
||||
);
|
||||
}
|
||||
|
||||
function encodeRepayMessage(RepayMessage memory message)
|
||||
internal
|
||||
pure
|
||||
returns (bytes memory)
|
||||
{
|
||||
return
|
||||
abi.encodePacked(
|
||||
uint8(3), // payloadID
|
||||
encodeMessageHeader(message.header),
|
||||
message.repayAmount,
|
||||
message.targetInterestAccrualIndex,
|
||||
message.repayTimestamp,
|
||||
message.paidInFull
|
||||
);
|
||||
}
|
||||
|
||||
function encodeLiquidationIntentMessage(
|
||||
LiquidationIntentMessage memory message
|
||||
) internal pure returns (bytes memory) {
|
||||
return
|
||||
abi.encodePacked(
|
||||
uint8(4), // payloadID
|
||||
encodeMessageHeader(message.header)
|
||||
);
|
||||
}
|
||||
|
||||
function decodeMessageHeader(bytes memory serialized)
|
||||
internal
|
||||
pure
|
||||
returns (MessageHeader memory header)
|
||||
{
|
||||
uint256 index = 0;
|
||||
|
||||
// parse the header
|
||||
header.payloadID = serialized.toUint8(index += 1);
|
||||
header.borrower = serialized.toAddress(index += 20);
|
||||
header.collateralAddress = serialized.toAddress(index += 20);
|
||||
header.borrowAddress = serialized.toAddress(index += 20);
|
||||
}
|
||||
|
||||
function decodeBorrowMessage(bytes memory serialized)
|
||||
internal
|
||||
pure
|
||||
returns (BorrowMessage memory params)
|
||||
{
|
||||
uint256 index = 0;
|
||||
|
||||
// parse the message header
|
||||
params.header = decodeMessageHeader(
|
||||
serialized.slice(index, index += 61)
|
||||
);
|
||||
params.borrowAmount = serialized.toUint256(index += 32);
|
||||
params.totalNormalizedBorrowAmount = serialized.toUint256(index += 32);
|
||||
params.interestAccrualIndex = serialized.toUint256(index += 32);
|
||||
|
||||
require(params.header.payloadID == 1, "invalid message");
|
||||
require(index == serialized.length, "index != serialized.length");
|
||||
}
|
||||
|
||||
function decodeRevertBorrowMessage(bytes memory serialized)
|
||||
internal
|
||||
pure
|
||||
returns (RevertBorrowMessage memory params)
|
||||
{
|
||||
uint256 index = 0;
|
||||
|
||||
// parse the message header
|
||||
params.header = decodeMessageHeader(
|
||||
serialized.slice(index, index += 61)
|
||||
);
|
||||
params.borrowAmount = serialized.toUint256(index += 32);
|
||||
params.sourceInterestAccrualIndex = serialized.toUint256(index += 32);
|
||||
|
||||
require(params.header.payloadID == 2, "invalid message");
|
||||
require(index == serialized.length, "index != serialized.length");
|
||||
}
|
||||
|
||||
function decodeRepayMessage(bytes memory serialized)
|
||||
internal
|
||||
pure
|
||||
returns (RepayMessage memory params)
|
||||
{
|
||||
uint256 index = 0;
|
||||
|
||||
// parse the message header
|
||||
params.header = decodeMessageHeader(
|
||||
serialized.slice(index, index += 61)
|
||||
);
|
||||
params.repayAmount = serialized.toUint256(index += 32);
|
||||
params.targetInterestAccrualIndex = serialized.toUint256(index += 32);
|
||||
params.repayTimestamp = serialized.toUint256(index += 32);
|
||||
params.paidInFull = serialized.toUint8(index += 1);
|
||||
|
||||
require(params.header.payloadID == 3, "invalid message");
|
||||
require(index == serialized.length, "index != serialized.length");
|
||||
}
|
||||
|
||||
function decodeLiquidationIntentMessage(bytes memory serialized)
|
||||
internal
|
||||
pure
|
||||
returns (LiquidationIntentMessage memory params)
|
||||
{
|
||||
uint256 index = 0;
|
||||
|
||||
// parse the message header
|
||||
params.header = decodeMessageHeader(
|
||||
serialized.slice(index, index += 61)
|
||||
);
|
||||
|
||||
// TODO: deserialize the LiquidationIntentMessage when implemented
|
||||
|
||||
require(params.header.payloadID == 4, "invalid message");
|
||||
require(index == serialized.length, "index != serialized.length");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import {NormalizedAmounts, NormalizedTotalAmounts, InterestRateModel} from "./CrossChainBorrowLendStructs.sol";
|
||||
|
||||
contract CrossChainBorrowLendStorage {
|
||||
struct State {
|
||||
// wormhole things
|
||||
address wormholeContractAddress;
|
||||
uint8 consistencyLevel;
|
||||
uint16 targetChainId;
|
||||
// precision variables
|
||||
uint256 collateralizationRatioPrecision;
|
||||
uint256 interestRatePrecision;
|
||||
// mock pyth price oracle
|
||||
address mockPythAddress;
|
||||
bytes32 targetContractAddress;
|
||||
// borrow and lend activity
|
||||
address collateralAssetAddress;
|
||||
bytes32 collateralAssetPythId;
|
||||
uint256 collateralizationRatio;
|
||||
address borrowingAssetAddress;
|
||||
uint256 interestAccrualIndex;
|
||||
uint256 interestAccrualIndexPrecision;
|
||||
uint256 lastActivityBlockTimestamp;
|
||||
NormalizedTotalAmounts totalAssets;
|
||||
uint256 repayGracePeriod;
|
||||
mapping(address => NormalizedAmounts) accountAssets;
|
||||
bytes32 borrowingAssetPythId;
|
||||
mapping(bytes32 => bool) consumedMessages;
|
||||
InterestRateModel interestRateModel;
|
||||
}
|
||||
}
|
||||
|
||||
contract CrossChainBorrowLendState {
|
||||
CrossChainBorrowLendStorage.State state;
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
struct DepositedBorrowedUints {
|
||||
uint256 deposited;
|
||||
uint256 borrowed;
|
||||
}
|
||||
|
||||
struct NormalizedTotalAmounts {
|
||||
uint256 deposited;
|
||||
uint256 borrowed;
|
||||
}
|
||||
|
||||
struct NormalizedAmounts {
|
||||
uint256 sourceDeposited;
|
||||
uint256 sourceBorrowed;
|
||||
uint256 targetDeposited;
|
||||
uint256 targetBorrowed;
|
||||
}
|
||||
|
||||
struct MessageHeader {
|
||||
uint8 payloadID;
|
||||
// address of the borrower
|
||||
address borrower;
|
||||
// collateral info
|
||||
address collateralAddress; // for verification
|
||||
// borrow info
|
||||
address borrowAddress; // for verification
|
||||
}
|
||||
|
||||
struct BorrowMessage {
|
||||
// payloadID = 1
|
||||
MessageHeader header;
|
||||
uint256 borrowAmount;
|
||||
uint256 totalNormalizedBorrowAmount;
|
||||
uint256 interestAccrualIndex;
|
||||
}
|
||||
|
||||
struct RevertBorrowMessage {
|
||||
// payloadID = 2
|
||||
MessageHeader header;
|
||||
uint256 borrowAmount;
|
||||
uint256 sourceInterestAccrualIndex;
|
||||
}
|
||||
|
||||
struct RepayMessage {
|
||||
// payloadID = 3
|
||||
MessageHeader header;
|
||||
uint256 repayAmount;
|
||||
uint256 targetInterestAccrualIndex;
|
||||
uint256 repayTimestamp;
|
||||
uint8 paidInFull;
|
||||
}
|
||||
|
||||
struct LiquidationIntentMessage {
|
||||
// payloadID = 4
|
||||
MessageHeader header;
|
||||
// TODO: add necessary variables
|
||||
}
|
||||
|
||||
struct InterestRateModel {
|
||||
uint64 ratePrecision;
|
||||
uint64 rateIntercept;
|
||||
uint64 rateCoefficientA;
|
||||
// TODO: add more complexity for example?
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// SPDX-License-Identifier: Apache 2
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
interface IMockPyth {
|
||||
struct Price {
|
||||
int64 price;
|
||||
uint64 conf;
|
||||
int32 expo;
|
||||
uint publishTime;
|
||||
}
|
||||
|
||||
struct PriceFeed {
|
||||
bytes32 id;
|
||||
Price price;
|
||||
Price emaPrice;
|
||||
}
|
||||
|
||||
struct PriceInfo {
|
||||
uint256 attestationTime;
|
||||
uint256 arrivalTime;
|
||||
uint256 arrivalBlock;
|
||||
PriceFeed priceFeed;
|
||||
}
|
||||
|
||||
function queryPriceFeed(bytes32 id) external view returns (PriceFeed memory priceFeed);
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
// contracts/Messages.sol
|
||||
// SPDX-License-Identifier: Apache 2
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
interface IWormhole {
|
||||
struct Signature {
|
||||
bytes32 r;
|
||||
bytes32 s;
|
||||
uint8 v;
|
||||
uint8 guardianIndex;
|
||||
}
|
||||
|
||||
struct VM {
|
||||
uint8 version;
|
||||
uint32 timestamp;
|
||||
uint32 nonce;
|
||||
uint16 emitterChainId;
|
||||
bytes32 emitterAddress;
|
||||
uint64 sequence;
|
||||
uint8 consistencyLevel;
|
||||
bytes payload;
|
||||
uint32 guardianSetIndex;
|
||||
Signature[] signatures;
|
||||
bytes32 hash;
|
||||
}
|
||||
|
||||
event LogMessagePublished(
|
||||
address indexed sender,
|
||||
uint64 sequence,
|
||||
uint32 nonce,
|
||||
bytes payload,
|
||||
uint8 consistencyLevel
|
||||
);
|
||||
|
||||
function publishMessage(
|
||||
uint32 nonce,
|
||||
bytes memory payload,
|
||||
uint8 consistencyLevel
|
||||
) external payable returns (uint64 sequence);
|
||||
|
||||
function parseAndVerifyVM(bytes calldata encodedVM)
|
||||
external
|
||||
view
|
||||
returns (
|
||||
VM memory vm,
|
||||
bool valid,
|
||||
string memory reason
|
||||
);
|
||||
|
||||
function chainId() external view returns (uint16);
|
||||
|
||||
function messageFee() external view returns (uint256);
|
||||
}
|
|
@ -0,0 +1,510 @@
|
|||
// SPDX-License-Identifier: Unlicense
|
||||
/*
|
||||
* @title Solidity Bytes Arrays Utils
|
||||
* @author Gonçalo Sá <goncalo.sa@consensys.net>
|
||||
*
|
||||
* @dev Bytes tightly packed arrays utility library for ethereum contracts written in Solidity.
|
||||
* The library lets you concatenate, slice and type cast bytes arrays both in memory and storage.
|
||||
*/
|
||||
pragma solidity >=0.8.0 <0.9.0;
|
||||
|
||||
|
||||
library BytesLib {
|
||||
function concat(
|
||||
bytes memory _preBytes,
|
||||
bytes memory _postBytes
|
||||
)
|
||||
internal
|
||||
pure
|
||||
returns (bytes memory)
|
||||
{
|
||||
bytes memory tempBytes;
|
||||
|
||||
assembly {
|
||||
// Get a location of some free memory and store it in tempBytes as
|
||||
// Solidity does for memory variables.
|
||||
tempBytes := mload(0x40)
|
||||
|
||||
// Store the length of the first bytes array at the beginning of
|
||||
// the memory for tempBytes.
|
||||
let length := mload(_preBytes)
|
||||
mstore(tempBytes, length)
|
||||
|
||||
// Maintain a memory counter for the current write location in the
|
||||
// temp bytes array by adding the 32 bytes for the array length to
|
||||
// the starting location.
|
||||
let mc := add(tempBytes, 0x20)
|
||||
// Stop copying when the memory counter reaches the length of the
|
||||
// first bytes array.
|
||||
let end := add(mc, length)
|
||||
|
||||
for {
|
||||
// Initialize a copy counter to the start of the _preBytes data,
|
||||
// 32 bytes into its memory.
|
||||
let cc := add(_preBytes, 0x20)
|
||||
} lt(mc, end) {
|
||||
// Increase both counters by 32 bytes each iteration.
|
||||
mc := add(mc, 0x20)
|
||||
cc := add(cc, 0x20)
|
||||
} {
|
||||
// Write the _preBytes data into the tempBytes memory 32 bytes
|
||||
// at a time.
|
||||
mstore(mc, mload(cc))
|
||||
}
|
||||
|
||||
// Add the length of _postBytes to the current length of tempBytes
|
||||
// and store it as the new length in the first 32 bytes of the
|
||||
// tempBytes memory.
|
||||
length := mload(_postBytes)
|
||||
mstore(tempBytes, add(length, mload(tempBytes)))
|
||||
|
||||
// Move the memory counter back from a multiple of 0x20 to the
|
||||
// actual end of the _preBytes data.
|
||||
mc := end
|
||||
// Stop copying when the memory counter reaches the new combined
|
||||
// length of the arrays.
|
||||
end := add(mc, length)
|
||||
|
||||
for {
|
||||
let cc := add(_postBytes, 0x20)
|
||||
} lt(mc, end) {
|
||||
mc := add(mc, 0x20)
|
||||
cc := add(cc, 0x20)
|
||||
} {
|
||||
mstore(mc, mload(cc))
|
||||
}
|
||||
|
||||
// Update the free-memory pointer by padding our last write location
|
||||
// to 32 bytes: add 31 bytes to the end of tempBytes to move to the
|
||||
// next 32 byte block, then round down to the nearest multiple of
|
||||
// 32. If the sum of the length of the two arrays is zero then add
|
||||
// one before rounding down to leave a blank 32 bytes (the length block with 0).
|
||||
mstore(0x40, and(
|
||||
add(add(end, iszero(add(length, mload(_preBytes)))), 31),
|
||||
not(31) // Round down to the nearest 32 bytes.
|
||||
))
|
||||
}
|
||||
|
||||
return tempBytes;
|
||||
}
|
||||
|
||||
function concatStorage(bytes storage _preBytes, bytes memory _postBytes) internal {
|
||||
assembly {
|
||||
// Read the first 32 bytes of _preBytes storage, which is the length
|
||||
// of the array. (We don't need to use the offset into the slot
|
||||
// because arrays use the entire slot.)
|
||||
let fslot := sload(_preBytes.slot)
|
||||
// Arrays of 31 bytes or less have an even value in their slot,
|
||||
// while longer arrays have an odd value. The actual length is
|
||||
// the slot divided by two for odd values, and the lowest order
|
||||
// byte divided by two for even values.
|
||||
// If the slot is even, bitwise and the slot with 255 and divide by
|
||||
// two to get the length. If the slot is odd, bitwise and the slot
|
||||
// with -1 and divide by two.
|
||||
let slength := div(and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), 2)
|
||||
let mlength := mload(_postBytes)
|
||||
let newlength := add(slength, mlength)
|
||||
// slength can contain both the length and contents of the array
|
||||
// if length < 32 bytes so let's prepare for that
|
||||
// v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage
|
||||
switch add(lt(slength, 32), lt(newlength, 32))
|
||||
case 2 {
|
||||
// Since the new array still fits in the slot, we just need to
|
||||
// update the contents of the slot.
|
||||
// uint256(bytes_storage) = uint256(bytes_storage) + uint256(bytes_memory) + new_length
|
||||
sstore(
|
||||
_preBytes.slot,
|
||||
// all the modifications to the slot are inside this
|
||||
// next block
|
||||
add(
|
||||
// we can just add to the slot contents because the
|
||||
// bytes we want to change are the LSBs
|
||||
fslot,
|
||||
add(
|
||||
mul(
|
||||
div(
|
||||
// load the bytes from memory
|
||||
mload(add(_postBytes, 0x20)),
|
||||
// zero all bytes to the right
|
||||
exp(0x100, sub(32, mlength))
|
||||
),
|
||||
// and now shift left the number of bytes to
|
||||
// leave space for the length in the slot
|
||||
exp(0x100, sub(32, newlength))
|
||||
),
|
||||
// increase length by the double of the memory
|
||||
// bytes length
|
||||
mul(mlength, 2)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
case 1 {
|
||||
// The stored value fits in the slot, but the combined value
|
||||
// will exceed it.
|
||||
// get the keccak hash to get the contents of the array
|
||||
mstore(0x0, _preBytes.slot)
|
||||
let sc := add(keccak256(0x0, 0x20), div(slength, 32))
|
||||
|
||||
// save new length
|
||||
sstore(_preBytes.slot, add(mul(newlength, 2), 1))
|
||||
|
||||
// The contents of the _postBytes array start 32 bytes into
|
||||
// the structure. Our first read should obtain the `submod`
|
||||
// bytes that can fit into the unused space in the last word
|
||||
// of the stored array. To get this, we read 32 bytes starting
|
||||
// from `submod`, so the data we read overlaps with the array
|
||||
// contents by `submod` bytes. Masking the lowest-order
|
||||
// `submod` bytes allows us to add that value directly to the
|
||||
// stored value.
|
||||
|
||||
let submod := sub(32, slength)
|
||||
let mc := add(_postBytes, submod)
|
||||
let end := add(_postBytes, mlength)
|
||||
let mask := sub(exp(0x100, submod), 1)
|
||||
|
||||
sstore(
|
||||
sc,
|
||||
add(
|
||||
and(
|
||||
fslot,
|
||||
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00
|
||||
),
|
||||
and(mload(mc), mask)
|
||||
)
|
||||
)
|
||||
|
||||
for {
|
||||
mc := add(mc, 0x20)
|
||||
sc := add(sc, 1)
|
||||
} lt(mc, end) {
|
||||
sc := add(sc, 1)
|
||||
mc := add(mc, 0x20)
|
||||
} {
|
||||
sstore(sc, mload(mc))
|
||||
}
|
||||
|
||||
mask := exp(0x100, sub(mc, end))
|
||||
|
||||
sstore(sc, mul(div(mload(mc), mask), mask))
|
||||
}
|
||||
default {
|
||||
// get the keccak hash to get the contents of the array
|
||||
mstore(0x0, _preBytes.slot)
|
||||
// Start copying to the last used word of the stored array.
|
||||
let sc := add(keccak256(0x0, 0x20), div(slength, 32))
|
||||
|
||||
// save new length
|
||||
sstore(_preBytes.slot, add(mul(newlength, 2), 1))
|
||||
|
||||
// Copy over the first `submod` bytes of the new data as in
|
||||
// case 1 above.
|
||||
let slengthmod := mod(slength, 32)
|
||||
let mlengthmod := mod(mlength, 32)
|
||||
let submod := sub(32, slengthmod)
|
||||
let mc := add(_postBytes, submod)
|
||||
let end := add(_postBytes, mlength)
|
||||
let mask := sub(exp(0x100, submod), 1)
|
||||
|
||||
sstore(sc, add(sload(sc), and(mload(mc), mask)))
|
||||
|
||||
for {
|
||||
sc := add(sc, 1)
|
||||
mc := add(mc, 0x20)
|
||||
} lt(mc, end) {
|
||||
sc := add(sc, 1)
|
||||
mc := add(mc, 0x20)
|
||||
} {
|
||||
sstore(sc, mload(mc))
|
||||
}
|
||||
|
||||
mask := exp(0x100, sub(mc, end))
|
||||
|
||||
sstore(sc, mul(div(mload(mc), mask), mask))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function slice(
|
||||
bytes memory _bytes,
|
||||
uint256 _start,
|
||||
uint256 _length
|
||||
)
|
||||
internal
|
||||
pure
|
||||
returns (bytes memory)
|
||||
{
|
||||
require(_length + 31 >= _length, "slice_overflow");
|
||||
require(_bytes.length >= _start + _length, "slice_outOfBounds");
|
||||
|
||||
bytes memory tempBytes;
|
||||
|
||||
assembly {
|
||||
switch iszero(_length)
|
||||
case 0 {
|
||||
// Get a location of some free memory and store it in tempBytes as
|
||||
// Solidity does for memory variables.
|
||||
tempBytes := mload(0x40)
|
||||
|
||||
// The first word of the slice result is potentially a partial
|
||||
// word read from the original array. To read it, we calculate
|
||||
// the length of that partial word and start copying that many
|
||||
// bytes into the array. The first word we copy will start with
|
||||
// data we don't care about, but the last `lengthmod` bytes will
|
||||
// land at the beginning of the contents of the new array. When
|
||||
// we're done copying, we overwrite the full first word with
|
||||
// the actual length of the slice.
|
||||
let lengthmod := and(_length, 31)
|
||||
|
||||
// The multiplication in the next line is necessary
|
||||
// because when slicing multiples of 32 bytes (lengthmod == 0)
|
||||
// the following copy loop was copying the origin's length
|
||||
// and then ending prematurely not copying everything it should.
|
||||
let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod)))
|
||||
let end := add(mc, _length)
|
||||
|
||||
for {
|
||||
// The multiplication in the next line has the same exact purpose
|
||||
// as the one above.
|
||||
let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start)
|
||||
} lt(mc, end) {
|
||||
mc := add(mc, 0x20)
|
||||
cc := add(cc, 0x20)
|
||||
} {
|
||||
mstore(mc, mload(cc))
|
||||
}
|
||||
|
||||
mstore(tempBytes, _length)
|
||||
|
||||
//update free-memory pointer
|
||||
//allocating the array padded to 32 bytes like the compiler does now
|
||||
mstore(0x40, and(add(mc, 31), not(31)))
|
||||
}
|
||||
//if we want a zero-length slice let's just return a zero-length array
|
||||
default {
|
||||
tempBytes := mload(0x40)
|
||||
//zero out the 32 bytes slice we are about to return
|
||||
//we need to do it because Solidity does not garbage collect
|
||||
mstore(tempBytes, 0)
|
||||
|
||||
mstore(0x40, add(tempBytes, 0x20))
|
||||
}
|
||||
}
|
||||
|
||||
return tempBytes;
|
||||
}
|
||||
|
||||
function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) {
|
||||
require(_bytes.length >= _start + 20, "toAddress_outOfBounds");
|
||||
address tempAddress;
|
||||
|
||||
assembly {
|
||||
tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000)
|
||||
}
|
||||
|
||||
return tempAddress;
|
||||
}
|
||||
|
||||
function toUint8(bytes memory _bytes, uint256 _start) internal pure returns (uint8) {
|
||||
require(_bytes.length >= _start + 1 , "toUint8_outOfBounds");
|
||||
uint8 tempUint;
|
||||
|
||||
assembly {
|
||||
tempUint := mload(add(add(_bytes, 0x1), _start))
|
||||
}
|
||||
|
||||
return tempUint;
|
||||
}
|
||||
|
||||
function toUint16(bytes memory _bytes, uint256 _start) internal pure returns (uint16) {
|
||||
require(_bytes.length >= _start + 2, "toUint16_outOfBounds");
|
||||
uint16 tempUint;
|
||||
|
||||
assembly {
|
||||
tempUint := mload(add(add(_bytes, 0x2), _start))
|
||||
}
|
||||
|
||||
return tempUint;
|
||||
}
|
||||
|
||||
function toUint32(bytes memory _bytes, uint256 _start) internal pure returns (uint32) {
|
||||
require(_bytes.length >= _start + 4, "toUint32_outOfBounds");
|
||||
uint32 tempUint;
|
||||
|
||||
assembly {
|
||||
tempUint := mload(add(add(_bytes, 0x4), _start))
|
||||
}
|
||||
|
||||
return tempUint;
|
||||
}
|
||||
|
||||
function toUint64(bytes memory _bytes, uint256 _start) internal pure returns (uint64) {
|
||||
require(_bytes.length >= _start + 8, "toUint64_outOfBounds");
|
||||
uint64 tempUint;
|
||||
|
||||
assembly {
|
||||
tempUint := mload(add(add(_bytes, 0x8), _start))
|
||||
}
|
||||
|
||||
return tempUint;
|
||||
}
|
||||
|
||||
function toUint96(bytes memory _bytes, uint256 _start) internal pure returns (uint96) {
|
||||
require(_bytes.length >= _start + 12, "toUint96_outOfBounds");
|
||||
uint96 tempUint;
|
||||
|
||||
assembly {
|
||||
tempUint := mload(add(add(_bytes, 0xc), _start))
|
||||
}
|
||||
|
||||
return tempUint;
|
||||
}
|
||||
|
||||
function toUint128(bytes memory _bytes, uint256 _start) internal pure returns (uint128) {
|
||||
require(_bytes.length >= _start + 16, "toUint128_outOfBounds");
|
||||
uint128 tempUint;
|
||||
|
||||
assembly {
|
||||
tempUint := mload(add(add(_bytes, 0x10), _start))
|
||||
}
|
||||
|
||||
return tempUint;
|
||||
}
|
||||
|
||||
function toUint256(bytes memory _bytes, uint256 _start) internal pure returns (uint256) {
|
||||
require(_bytes.length >= _start + 32, "toUint256_outOfBounds");
|
||||
uint256 tempUint;
|
||||
|
||||
assembly {
|
||||
tempUint := mload(add(add(_bytes, 0x20), _start))
|
||||
}
|
||||
|
||||
return tempUint;
|
||||
}
|
||||
|
||||
function toBytes32(bytes memory _bytes, uint256 _start) internal pure returns (bytes32) {
|
||||
require(_bytes.length >= _start + 32, "toBytes32_outOfBounds");
|
||||
bytes32 tempBytes32;
|
||||
|
||||
assembly {
|
||||
tempBytes32 := mload(add(add(_bytes, 0x20), _start))
|
||||
}
|
||||
|
||||
return tempBytes32;
|
||||
}
|
||||
|
||||
function equal(bytes memory _preBytes, bytes memory _postBytes) internal pure returns (bool) {
|
||||
bool success = true;
|
||||
|
||||
assembly {
|
||||
let length := mload(_preBytes)
|
||||
|
||||
// if lengths don't match the arrays are not equal
|
||||
switch eq(length, mload(_postBytes))
|
||||
case 1 {
|
||||
// cb is a circuit breaker in the for loop since there's
|
||||
// no said feature for inline assembly loops
|
||||
// cb = 1 - don't breaker
|
||||
// cb = 0 - break
|
||||
let cb := 1
|
||||
|
||||
let mc := add(_preBytes, 0x20)
|
||||
let end := add(mc, length)
|
||||
|
||||
for {
|
||||
let cc := add(_postBytes, 0x20)
|
||||
// the next line is the loop condition:
|
||||
// while(uint256(mc < end) + cb == 2)
|
||||
} eq(add(lt(mc, end), cb), 2) {
|
||||
mc := add(mc, 0x20)
|
||||
cc := add(cc, 0x20)
|
||||
} {
|
||||
// if any of these checks fails then arrays are not equal
|
||||
if iszero(eq(mload(mc), mload(cc))) {
|
||||
// unsuccess:
|
||||
success := 0
|
||||
cb := 0
|
||||
}
|
||||
}
|
||||
}
|
||||
default {
|
||||
// unsuccess:
|
||||
success := 0
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
function equalStorage(
|
||||
bytes storage _preBytes,
|
||||
bytes memory _postBytes
|
||||
)
|
||||
internal
|
||||
view
|
||||
returns (bool)
|
||||
{
|
||||
bool success = true;
|
||||
|
||||
assembly {
|
||||
// we know _preBytes_offset is 0
|
||||
let fslot := sload(_preBytes.slot)
|
||||
// Decode the length of the stored array like in concatStorage().
|
||||
let slength := div(and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), 2)
|
||||
let mlength := mload(_postBytes)
|
||||
|
||||
// if lengths don't match the arrays are not equal
|
||||
switch eq(slength, mlength)
|
||||
case 1 {
|
||||
// slength can contain both the length and contents of the array
|
||||
// if length < 32 bytes so let's prepare for that
|
||||
// v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage
|
||||
if iszero(iszero(slength)) {
|
||||
switch lt(slength, 32)
|
||||
case 1 {
|
||||
// blank the last byte which is the length
|
||||
fslot := mul(div(fslot, 0x100), 0x100)
|
||||
|
||||
if iszero(eq(fslot, mload(add(_postBytes, 0x20)))) {
|
||||
// unsuccess:
|
||||
success := 0
|
||||
}
|
||||
}
|
||||
default {
|
||||
// cb is a circuit breaker in the for loop since there's
|
||||
// no said feature for inline assembly loops
|
||||
// cb = 1 - don't breaker
|
||||
// cb = 0 - break
|
||||
let cb := 1
|
||||
|
||||
// get the keccak hash to get the contents of the array
|
||||
mstore(0x0, _preBytes.slot)
|
||||
let sc := keccak256(0x0, 0x20)
|
||||
|
||||
let mc := add(_postBytes, 0x20)
|
||||
let end := add(mc, mlength)
|
||||
|
||||
// the next line is the loop condition:
|
||||
// while(uint256(mc < end) + cb == 2)
|
||||
for {} eq(add(lt(mc, end), cb), 2) {
|
||||
sc := add(sc, 1)
|
||||
mc := add(mc, 0x20)
|
||||
} {
|
||||
if iszero(eq(sload(sc), mload(mc))) {
|
||||
// unsuccess:
|
||||
success := 0
|
||||
cb := 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
default {
|
||||
// unsuccess:
|
||||
success := 0
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
pragma solidity ^0.8.0;
|
||||
|
||||
contract MockPyth {
|
||||
// Mapping of cached price information
|
||||
// priceId => PriceInfo
|
||||
mapping(bytes32 => PriceInfo) latestPriceInfo;
|
||||
|
||||
// A price with a degree of uncertainty, represented as a price +- a confidence interval.
|
||||
//
|
||||
// The confidence interval roughly corresponds to the standard error of a normal distribution.
|
||||
// Both the price and confidence are stored in a fixed-point numeric representation,
|
||||
// `x * (10^expo)`, where `expo` is the exponent.
|
||||
//
|
||||
// Please refer to the documentation at https://docs.pyth.network/consumers/best-practices for how
|
||||
// to how this price safely.
|
||||
struct Price {
|
||||
// Price
|
||||
int64 price;
|
||||
// Confidence interval around the price
|
||||
uint64 conf;
|
||||
// Price exponent
|
||||
int32 expo;
|
||||
// Unix timestamp describing when the price was published
|
||||
uint publishTime;
|
||||
}
|
||||
|
||||
// PriceFeed represents a current aggregate price from pyth publisher feeds.
|
||||
struct PriceFeed {
|
||||
// The price ID.
|
||||
bytes32 id;
|
||||
// Latest available price
|
||||
Price price;
|
||||
// Latest available exponentially-weighted moving average price
|
||||
Price emaPrice;
|
||||
}
|
||||
|
||||
struct PriceInfo {
|
||||
uint256 attestationTime;
|
||||
uint256 arrivalTime;
|
||||
uint256 arrivalBlock;
|
||||
PriceFeed priceFeed;
|
||||
}
|
||||
|
||||
function setLatestPriceInfo(bytes32 priceId, PriceInfo memory info) internal {
|
||||
latestPriceInfo[priceId] = info;
|
||||
}
|
||||
|
||||
function getLatestPriceInfo(bytes32 priceId) internal view returns (PriceInfo memory info){
|
||||
return latestPriceInfo[priceId];
|
||||
}
|
||||
|
||||
function queryPriceFeed(bytes32 id) public view returns (PriceFeed memory priceFeed){
|
||||
// Look up the latest price info for the given ID
|
||||
PriceInfo memory info = getLatestPriceInfo(id);
|
||||
require(info.priceFeed.id != 0, "no price feed found for the given price id");
|
||||
|
||||
return info.priceFeed;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
// SPDX-License-Identifier: Apache 2
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
import {NormalizedAmounts, NormalizedTotalAmounts} from "../src/CrossChainBorrowLendStructs.sol";
|
||||
import {ExposedCrossChainBorrowLend} from "./helpers/ExposedCrossChainBorrowLend.sol";
|
||||
import {MyERC20} from "./helpers/MyERC20.sol";
|
||||
import "forge-std/Test.sol";
|
||||
|
||||
import "forge-std/console.sol";
|
||||
|
||||
contract CrossChainBorrowLendTest is Test {
|
||||
MyERC20 collateralToken;
|
||||
MyERC20 borrowedAssetToken;
|
||||
ExposedCrossChainBorrowLend borrowLendContract;
|
||||
|
||||
bytes32 collateralAssetPythId;
|
||||
bytes32 borrowingAssetPythId;
|
||||
uint256 collateralizationRatio;
|
||||
|
||||
function setUp() public {
|
||||
address wormholeAddress = msg.sender;
|
||||
address mockPythAddress = msg.sender;
|
||||
bytes32 targetContractAddress = 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef;
|
||||
|
||||
collateralToken = new MyERC20("WBNB", "WBNB", 18);
|
||||
borrowedAssetToken = new MyERC20("USDC", "USDC", 6);
|
||||
|
||||
// 80%
|
||||
collateralizationRatio = 0.8e18;
|
||||
|
||||
// TODO
|
||||
borrowLendContract = new ExposedCrossChainBorrowLend(
|
||||
wormholeAddress,
|
||||
1, // consistencyLevel
|
||||
mockPythAddress,
|
||||
2, // targetChainId (ethereum)
|
||||
targetContractAddress,
|
||||
address(collateralToken), // collateralAsset
|
||||
collateralAssetPythId,
|
||||
collateralizationRatio,
|
||||
address(borrowedAssetToken),
|
||||
borrowingAssetPythId,
|
||||
5 * 60 // gracePeriod (5 minutes)
|
||||
);
|
||||
}
|
||||
|
||||
function testComputeInterestProportion() public {
|
||||
// start from zero
|
||||
vm.warp(0);
|
||||
uint256 timeStart = block.timestamp;
|
||||
|
||||
// warp to 1 year in the future
|
||||
vm.warp(365 * 24 * 60 * 60);
|
||||
uint256 secondsElapsed = block.timestamp - timeStart;
|
||||
|
||||
// accrue interest with intercept and coefficient
|
||||
uint256 intercept = 0.02e18; // 2% starting rate
|
||||
uint256 coefficient = 0.001e18; // increase 10 basis points per 1% borrowed
|
||||
|
||||
// fake supply some amount
|
||||
uint256 deposited = 100e6; // 100 USDC (6 decimals)
|
||||
borrowLendContract.HACKED_setTotalAssetsDeposited(deposited);
|
||||
|
||||
// fake borrow some amount
|
||||
uint256 borrowed = 50e6; // 50 USDC (6 decimals)
|
||||
borrowLendContract.HACKED_setTotalAssetsBorrowed(borrowed);
|
||||
|
||||
// we expect the interest accrued equal to the intercept
|
||||
uint256 interestProportion = borrowLendContract
|
||||
.EXPOSED_computeInterestProportion(
|
||||
secondsElapsed,
|
||||
intercept,
|
||||
coefficient
|
||||
);
|
||||
|
||||
// expect using the correct value (0.0205e18)
|
||||
{
|
||||
require(
|
||||
interestProportion == 0.0205e18,
|
||||
"interestProportion != expected"
|
||||
);
|
||||
}
|
||||
|
||||
// expect using calculation
|
||||
{
|
||||
uint256 expected = intercept + (coefficient * borrowed) / deposited;
|
||||
require(
|
||||
interestProportion == expected,
|
||||
"interestProportion != expected (computed)"
|
||||
);
|
||||
}
|
||||
|
||||
// clear
|
||||
borrowLendContract.HACKED_setTotalAssetsDeposited(0);
|
||||
borrowLendContract.HACKED_setTotalAssetsBorrowed(0);
|
||||
}
|
||||
|
||||
function testUpdateInterestAccrualIndex() public {
|
||||
// start from zero
|
||||
vm.warp(0);
|
||||
borrowLendContract.HACKED_setLastActivityBlockTimestamp(
|
||||
block.timestamp
|
||||
);
|
||||
|
||||
// fake supply some amount
|
||||
uint256 deposited = 200e6; // 200 USDC (6 decimals)
|
||||
borrowLendContract.HACKED_setTotalAssetsDeposited(deposited);
|
||||
|
||||
// fake borrow some amount
|
||||
uint256 borrowed = 20e6; // 20 USDC (6 decimals)
|
||||
borrowLendContract.HACKED_setTotalAssetsBorrowed(borrowed);
|
||||
|
||||
// warp to 1 year in the future
|
||||
vm.warp(365 * 24 * 60 * 60);
|
||||
|
||||
// trigger accrual
|
||||
borrowLendContract.EXPOSED_updateInterestAccrualIndex();
|
||||
|
||||
{
|
||||
// expect using the correct value (1.02e18)
|
||||
require(
|
||||
borrowLendContract.borrowedInterestAccrualIndex() == 1.02e18,
|
||||
"borrowedInterestAccrualIndex() != expected (first iteration)"
|
||||
);
|
||||
// expect using the correct value (1.002e18)
|
||||
require(
|
||||
borrowLendContract.collateralInterestAccrualIndex() == 1.002e18,
|
||||
"collateralInterestAccrualIndex() != expected (first iteration)"
|
||||
);
|
||||
}
|
||||
|
||||
// warp to 2 years in the future
|
||||
vm.warp(2 * 365 * 24 * 60 * 60);
|
||||
|
||||
// trigger accrual again
|
||||
borrowLendContract.EXPOSED_updateInterestAccrualIndex();
|
||||
|
||||
{
|
||||
// expect using the correct value (1.04e18)
|
||||
require(
|
||||
borrowLendContract.borrowedInterestAccrualIndex() == 1.04e18,
|
||||
"borrowedInterestAccrualIndex() != expected (second iteration)"
|
||||
);
|
||||
// expect using the correct value (1.004e18)
|
||||
require(
|
||||
borrowLendContract.collateralInterestAccrualIndex() == 1.004e18,
|
||||
"collateralInterestAccrualIndex() != expected (second iteration)"
|
||||
);
|
||||
}
|
||||
|
||||
// check denormalized deposit and borrowed. should be equal
|
||||
{
|
||||
NormalizedTotalAmounts memory amounts = borrowLendContract
|
||||
.normalizedAmounts();
|
||||
uint256 accruedDepositedInterest = borrowLendContract
|
||||
.denormalizeAmount(
|
||||
amounts.deposited,
|
||||
borrowLendContract.collateralInterestAccrualIndex()
|
||||
) - deposited;
|
||||
uint256 accruedBorrowedInterest = borrowLendContract
|
||||
.denormalizeAmount(
|
||||
amounts.borrowed,
|
||||
borrowLendContract.borrowedInterestAccrualIndex()
|
||||
) - borrowed;
|
||||
require(
|
||||
accruedDepositedInterest == accruedBorrowedInterest,
|
||||
"accruedDepositedInterest != accruedBorrowedInterest"
|
||||
);
|
||||
}
|
||||
|
||||
// clear
|
||||
borrowLendContract.HACKED_setTotalAssetsDeposited(0);
|
||||
borrowLendContract.HACKED_setTotalAssetsBorrowed(0);
|
||||
}
|
||||
|
||||
function testMaxAllowedToWithdraw() public {
|
||||
uint64 collateralPrice = 400; // WBNB
|
||||
uint64 borrowAssetPrice = 1; // USDC
|
||||
|
||||
uint256 deposited = 1e18; // 1 WBNB (18 decimals)
|
||||
borrowLendContract.HACKED_setAccountAssetsDeposited(
|
||||
msg.sender,
|
||||
deposited
|
||||
);
|
||||
|
||||
uint256 borrowed = 100e6; // 100 USDC (6 decimals)
|
||||
borrowLendContract.HACKED_setAccountAssetsBorrowed(
|
||||
msg.sender,
|
||||
borrowed
|
||||
);
|
||||
|
||||
uint256 maxAllowed = borrowLendContract
|
||||
.EXPOSED_maxAllowedToWithdrawWithPrices(
|
||||
msg.sender,
|
||||
collateralPrice,
|
||||
borrowAssetPrice
|
||||
);
|
||||
|
||||
// expect 0.6875e18 (0.6875 WBNB)
|
||||
{
|
||||
require(maxAllowed == 0.6875e18, "maxAllowed != expected");
|
||||
}
|
||||
|
||||
// clear
|
||||
borrowLendContract.HACKED_setAccountAssetsDeposited(msg.sender, 0);
|
||||
borrowLendContract.HACKED_setAccountAssetsBorrowed(msg.sender, 0);
|
||||
}
|
||||
|
||||
function testMaxAllowedToBorrow() public {
|
||||
uint64 collateralPrice = 400; // WBNB
|
||||
uint64 borrowAssetPrice = 1; // USDC
|
||||
|
||||
uint256 deposited = 1e18; // 1 WBNB (18 decimals)
|
||||
borrowLendContract.HACKED_setAccountAssetsDeposited(
|
||||
msg.sender,
|
||||
deposited
|
||||
);
|
||||
|
||||
uint256 borrowed = 100e6; // 100 USDC (6 decimals)
|
||||
borrowLendContract.HACKED_setAccountAssetsBorrowed(
|
||||
msg.sender,
|
||||
borrowed
|
||||
);
|
||||
|
||||
uint256 maxAllowed = borrowLendContract
|
||||
.EXPOSED_maxAllowedToBorrowWithPrices(
|
||||
msg.sender,
|
||||
collateralPrice,
|
||||
borrowAssetPrice
|
||||
);
|
||||
|
||||
// expect 220e6 (220 USDC)
|
||||
{
|
||||
require(maxAllowed == 220e6, "maxAllowed != expected");
|
||||
}
|
||||
|
||||
// clear
|
||||
borrowLendContract.HACKED_setAccountAssetsDeposited(msg.sender, 0);
|
||||
borrowLendContract.HACKED_setAccountAssetsBorrowed(msg.sender, 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
// SPDX-License-Identifier: Apache 2
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import {NormalizedAmounts} from "../../src/CrossChainBorrowLendStructs.sol";
|
||||
import {CrossChainBorrowLend} from "../../src/CrossChainBorrowLend.sol";
|
||||
import "forge-std/Test.sol";
|
||||
|
||||
import "forge-std/console.sol";
|
||||
|
||||
contract ExposedCrossChainBorrowLend is CrossChainBorrowLend {
|
||||
constructor(
|
||||
address wormholeContractAddress_,
|
||||
uint8 consistencyLevel_,
|
||||
address mockPythAddress_,
|
||||
uint16 targetChainId_,
|
||||
bytes32 targetContractAddress_,
|
||||
address collateralAsset_,
|
||||
bytes32 collateralAssetPythId_,
|
||||
uint256 collateralizationRatio_,
|
||||
address borrowingAsset_,
|
||||
bytes32 borrowingAssetPythId_,
|
||||
uint256 repayGracePeriod_
|
||||
)
|
||||
CrossChainBorrowLend(
|
||||
wormholeContractAddress_,
|
||||
consistencyLevel_,
|
||||
mockPythAddress_,
|
||||
targetChainId_,
|
||||
targetContractAddress_,
|
||||
collateralAsset_,
|
||||
collateralAssetPythId_,
|
||||
collateralizationRatio_,
|
||||
borrowingAsset_,
|
||||
borrowingAssetPythId_,
|
||||
repayGracePeriod_
|
||||
)
|
||||
{
|
||||
// nothing else
|
||||
}
|
||||
|
||||
function EXPOSED_computeInterestProportion(
|
||||
uint256 secondsElapsed,
|
||||
uint256 intercept,
|
||||
uint256 coefficient
|
||||
) external view returns (uint256) {
|
||||
return
|
||||
computeInterestProportion(secondsElapsed, intercept, coefficient);
|
||||
}
|
||||
|
||||
function EXPOSED_updateInterestAccrualIndex() external {
|
||||
return updateInterestAccrualIndex();
|
||||
}
|
||||
|
||||
function EXPOSED_maxAllowedToBorrowWithPrices(
|
||||
address account,
|
||||
uint64 collateralPrice,
|
||||
uint64 borrowAssetPrice
|
||||
) external view returns (uint256) {
|
||||
return
|
||||
maxAllowedToBorrowWithPrices(
|
||||
account,
|
||||
collateralPrice,
|
||||
borrowAssetPrice
|
||||
);
|
||||
}
|
||||
|
||||
function EXPOSED_maxAllowedToWithdrawWithPrices(
|
||||
address account,
|
||||
uint64 collateralPrice,
|
||||
uint64 borrowAssetPrice
|
||||
) external view returns (uint256) {
|
||||
return
|
||||
maxAllowedToWithdrawWithPrices(
|
||||
account,
|
||||
collateralPrice,
|
||||
borrowAssetPrice
|
||||
);
|
||||
}
|
||||
|
||||
function EXPOSED_accountAssets(address account)
|
||||
external
|
||||
view
|
||||
returns (NormalizedAmounts memory)
|
||||
{
|
||||
return state.accountAssets[account];
|
||||
}
|
||||
|
||||
function HACKED_setTotalAssetsDeposited(uint256 amount) external {
|
||||
state.totalAssets.deposited = amount;
|
||||
}
|
||||
|
||||
function HACKED_setTotalAssetsBorrowed(uint256 amount) external {
|
||||
state.totalAssets.borrowed = amount;
|
||||
}
|
||||
|
||||
function HACKED_setLastActivityBlockTimestamp(uint256 timestamp) external {
|
||||
state.lastActivityBlockTimestamp = timestamp;
|
||||
}
|
||||
|
||||
function HACKED_setAccountAssetsDeposited(address account, uint256 amount)
|
||||
external
|
||||
{
|
||||
state.accountAssets[account].sourceDeposited = amount;
|
||||
}
|
||||
|
||||
function HACKED_setAccountAssetsBorrowed(address account, uint256 amount)
|
||||
external
|
||||
{
|
||||
state.accountAssets[account].targetBorrowed = amount;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
// SPDX-License-Identifier: Apache 2
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
|
||||
contract MyERC20 is ERC20 {
|
||||
uint8 _decimals;
|
||||
|
||||
constructor(
|
||||
string memory name_,
|
||||
string memory symbol_,
|
||||
uint8 decimals_
|
||||
) ERC20(name_, symbol_) {
|
||||
_decimals = decimals_;
|
||||
}
|
||||
|
||||
function decimals() public view override returns (uint8) {
|
||||
return _decimals;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue