Add CrossChainBorrowLend (#1)

Co-authored-by: Reptile <43194093+dsterioti@users.noreply.github.com>
This commit is contained in:
Karl Kempe 2022-09-29 00:43:51 -05:00 committed by GitHub
parent f835511fbc
commit c8fac88a0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 3361 additions and 0 deletions

0
.gitmodules vendored Normal file
View File

6
evm/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
cache
lib
node_modules
out
venus-protocol
wormhole

25
evm/Makefile Normal file
View File

@ -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

11
evm/foundry.toml Normal file
View File

@ -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/",
]

28
evm/package.json Normal file
View File

@ -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"
}
}

View File

@ -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;
}
}

View File

@ -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
);
}
}

View File

@ -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");
}
}

View File

@ -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;
}

View File

@ -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?
}

View File

@ -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);
}

View File

@ -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);
}

510
evm/src/libraries/external/BytesLib.sol vendored Normal file
View File

@ -0,0 +1,510 @@
// SPDX-License-Identifier: Unlicense
/*
* @title Solidity Bytes Arrays Utils
* @author Gonçalo <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;
}
}

59
evm/src/pyth/MockPyth.sol Normal file
View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

1162
evm/yarn.lock Normal file

File diff suppressed because it is too large Load Diff