Rate model/integration (#2)

* evm: fix interest calculations and tests

* Add collateral change messages

Co-authored-by: Drew <dsterioti@users.noreply.github.com>
This commit is contained in:
Karl Kempe 2022-10-03 08:25:05 -05:00 committed by GitHub
parent 0fd688c7ff
commit f67eee8b5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 615 additions and 235 deletions

View File

@ -54,8 +54,12 @@ contract CrossChainBorrowLend is
// 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;
uint256 precision = 1e18;
state.interestAccrualIndexPrecision = precision;
state.interestAccrualIndex.source.deposited = precision;
state.interestAccrualIndex.source.borrowed = precision;
state.interestAccrualIndex.target.deposited = precision;
state.interestAccrualIndex.target.borrowed = precision;
// pyth oracle address and asset IDs
state.mockPythAddress = mockPythAddress_;
@ -66,19 +70,19 @@ contract CrossChainBorrowLend is
state.repayGracePeriod = repayGracePeriod_;
}
function addCollateral(uint256 amount) public nonReentrant {
function addCollateral(uint256 amount) public nonReentrant returns (uint64 sequence) {
require(amount > 0, "nothing to deposit");
// update current price index
updateInterestAccrualIndex();
updateSourceInterestAccrualIndex();
// update state for supplier
uint256 normalizedAmount = normalizeAmount(
amount,
collateralInterestAccrualIndex()
sourceCollateralInterestAccrualIndex()
);
state.accountAssets[_msgSender()].sourceDeposited += normalizedAmount;
state.totalAssets.deposited += normalizedAmount;
state.accountAssets[_msgSender()].source.deposited += normalizedAmount;
state.totalAssets.source.deposited += normalizedAmount;
SafeERC20.safeTransferFrom(
collateralToken(),
@ -86,13 +90,31 @@ contract CrossChainBorrowLend is
address(this),
amount
);
// construct wormhole message
MessageHeader memory header = MessageHeader({
payloadID: uint8(5),
sender: _msgSender(),
collateralAddress: state.collateralAssetAddress,
borrowAddress: state.borrowingAssetAddress
});
sequence = sendWormholeMessage(
encodeDepositChangeMessage(
DepositChangeMessage({
header: header,
depositType: DepositType.Add,
amount: normalizeAmount(amount, sourceCollateralInterestAccrualIndex())
})
)
);
}
function removeCollateral(uint256 amount) public nonReentrant {
function removeCollateral(uint256 amount) public nonReentrant returns (uint64 sequence) {
require(amount > 0, "nothing to withdraw");
// update current price index
updateInterestAccrualIndex();
updateSourceInterestAccrualIndex();
// Check if user has enough to withdraw from the contract
require(
@ -103,34 +125,47 @@ contract CrossChainBorrowLend is
// update state for supplier
uint256 normalizedAmount = normalizeAmount(
amount,
collateralInterestAccrualIndex()
sourceCollateralInterestAccrualIndex()
);
state.accountAssets[_msgSender()].sourceDeposited -= normalizedAmount;
state.totalAssets.deposited -= normalizedAmount;
state.accountAssets[_msgSender()].source.deposited -= normalizedAmount;
state.totalAssets.source.deposited -= normalizedAmount;
// transfer the tokens to the caller
SafeERC20.safeTransfer(collateralToken(), _msgSender(), amount);
// construct wormhole message
MessageHeader memory header = MessageHeader({
payloadID: uint8(5),
sender: _msgSender(),
collateralAddress: state.collateralAssetAddress,
borrowAddress: state.borrowingAssetAddress
});
sequence = sendWormholeMessage(
encodeDepositChangeMessage(
DepositChangeMessage({
header: header,
depositType: DepositType.Remove,
amount: normalizedAmount
})
)
);
}
function removeCollateralInFull() public nonReentrant {
function removeCollateralInFull() public nonReentrant returns (uint64 sequence) {
// fetch the account information for the caller
NormalizedAmounts memory normalizedAmounts = state.accountAssets[
_msgSender()
];
SourceTargetUints memory account = state.accountAssets[_msgSender()];
// make sure the account has closed all borrowed positions
require(
normalizedAmounts.targetBorrowed == 0,
"account has outstanding loans"
);
require(account.target.borrowed == 0, "account has outstanding loans");
// update current price index
updateInterestAccrualIndex();
updateSourceInterestAccrualIndex();
// update state for supplier
uint256 normalizedAmount = normalizedAmounts.sourceDeposited;
state.accountAssets[_msgSender()].sourceDeposited = 0;
state.totalAssets.deposited -= normalizedAmount;
uint256 normalizedAmount = account.source.deposited;
state.accountAssets[_msgSender()].source.deposited = 0;
state.totalAssets.source.deposited -= normalizedAmount;
// transfer the tokens to the caller
SafeERC20.safeTransfer(
@ -138,32 +173,124 @@ contract CrossChainBorrowLend is
_msgSender(),
denormalizeAmount(
normalizedAmount,
collateralInterestAccrualIndex()
sourceCollateralInterestAccrualIndex()
)
);
// construct wormhole message
MessageHeader memory header = MessageHeader({
payloadID: uint8(5),
sender: _msgSender(),
collateralAddress: state.collateralAssetAddress,
borrowAddress: state.borrowingAssetAddress
});
sequence = sendWormholeMessage(
encodeDepositChangeMessage(
DepositChangeMessage({
header: header,
depositType: DepositType.RemoveFull,
amount: normalizedAmount
})
)
);
}
function computeInterestProportion(
function completeCollateralChange(bytes memory encodedVm) public {
// 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 deposit change message
DepositChangeMessage memory params = decodeDepositChangeMessage(parsed.payload);
address depositor = params.header.sender;
// correct assets?
require(
params.header.collateralAddress == state.borrowingAssetAddress &&
params.header.borrowAddress == state.collateralAssetAddress,
"invalid asset metadata"
);
// update current price index
updateTargetInterestAccrualIndex();
// update this contracts state to reflect the deposit change
if (params.depositType == DepositType.Add) {
state.totalAssets.target.deposited += params.amount;
state.accountAssets[depositor].target.deposited += params.amount;
} else if (params.depositType == DepositType.Remove) {
state.totalAssets.target.deposited -= params.amount;
state.accountAssets[depositor].target.deposited -= params.amount;
} else if (params.depositType == DepositType.RemoveFull) {
// fetch the deposit amount from state
state.totalAssets.target.deposited -= state.accountAssets[depositor].target.deposited;
state.accountAssets[depositor].target.deposited = 0;
}
}
function computeSourceInterestFactor(
uint256 secondsElapsed,
uint256 intercept,
uint256 coefficient
) internal view returns (uint256) {
uint256 deposited = state.totalAssets.deposited;
return
_computeInterestFactor(
secondsElapsed,
intercept,
coefficient,
state.totalAssets.source.deposited,
state.totalAssets.source.borrowed
);
}
function computeTargetInterestFactor(
uint256 secondsElapsed,
uint256 intercept,
uint256 coefficient
) internal view returns (uint256) {
return
_computeInterestFactor(
secondsElapsed,
intercept,
coefficient,
state.totalAssets.target.deposited,
state.totalAssets.target.borrowed
);
}
function _computeInterestFactor(
uint256 secondsElapsed,
uint256 intercept,
uint256 coefficient,
uint256 deposited,
uint256 borrowed
) internal pure returns (uint256) {
if (deposited == 0) {
return 0;
}
return
(secondsElapsed *
(intercept +
(coefficient * state.totalAssets.borrowed) /
deposited)) /
(intercept + (coefficient * borrowed) / deposited)) /
365 /
24 /
60 /
60;
}
function updateInterestAccrualIndex() internal {
function updateSourceInterestAccrualIndex() internal {
// TODO: change to block.number?
uint256 secondsElapsed = block.timestamp -
state.lastActivityBlockTimestamp;
@ -176,25 +303,57 @@ contract CrossChainBorrowLend is
// Should not hit, but just here in case someone
// tries to update the interest when there is nothing
// deposited.
uint256 deposited = state.totalAssets.deposited;
uint256 deposited = state.totalAssets.source.deposited;
if (deposited == 0) {
return;
}
state.lastActivityBlockTimestamp = block.timestamp;
state.interestAccrualIndex += computeInterestProportion(
uint256 interestFactor = computeSourceInterestFactor(
secondsElapsed,
state.interestRateModel.rateIntercept,
state.interestRateModel.rateCoefficientA
);
state.interestAccrualIndex.source.borrowed += interestFactor;
state.interestAccrualIndex.source.deposited +=
(interestFactor * state.totalAssets.source.borrowed) /
deposited;
}
function updateTargetInterestAccrualIndex() internal {
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.target.deposited;
if (deposited == 0) {
return;
}
state.lastActivityBlockTimestamp = block.timestamp;
uint256 interestFactor = computeTargetInterestFactor(
secondsElapsed,
state.interestRateModel.rateIntercept,
state.interestRateModel.rateCoefficientA
);
state.interestAccrualIndex.target.borrowed += interestFactor;
state.interestAccrualIndex.target.deposited +=
(interestFactor * state.totalAssets.target.borrowed) /
deposited;
}
function initiateBorrow(uint256 amount) public returns (uint64 sequence) {
require(amount > 0, "nothing to borrow");
// update current price index
updateInterestAccrualIndex();
updateTargetInterestAccrualIndex();
// Check if user has enough to borrow
require(
@ -203,15 +362,15 @@ contract CrossChainBorrowLend is
);
// update state for borrower
uint256 borrowedIndex = borrowedInterestAccrualIndex();
uint256 borrowedIndex = targetBorrowedInterestAccrualIndex();
uint256 normalizedAmount = normalizeAmount(amount, borrowedIndex);
state.accountAssets[_msgSender()].targetBorrowed += normalizedAmount;
state.totalAssets.borrowed += normalizedAmount;
state.accountAssets[_msgSender()].target.borrowed += normalizedAmount;
state.totalAssets.target.borrowed += normalizedAmount;
// construct wormhole message
MessageHeader memory header = MessageHeader({
payloadID: uint8(1),
borrower: _msgSender(),
sender: _msgSender(),
collateralAddress: state.collateralAssetAddress,
borrowAddress: state.borrowingAssetAddress
});
@ -223,7 +382,8 @@ contract CrossChainBorrowLend is
borrowAmount: amount,
totalNormalizedBorrowAmount: state
.accountAssets[_msgSender()]
.targetBorrowed,
.target
.borrowed,
interestAccrualIndex: borrowedIndex
})
)
@ -252,23 +412,26 @@ contract CrossChainBorrowLend is
// decode borrow message
BorrowMessage memory params = decodeBorrowMessage(parsed.payload);
address borrower = params.header.sender;
// correct assets?
require(verifyAssetMetaFromBorrow(params), "invalid asset metadata");
// update current price index
updateSourceInterestAccrualIndex();
// make sure this contract has enough assets to fund the borrow
if (
params.borrowAmount >
denormalizeAmount(
normalizedLiquidity(),
borrowedInterestAccrualIndex()
)
normalizeAmount(
params.borrowAmount,
sourceBorrowedInterestAccrualIndex()
) > sourceLiquidity()
) {
// 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,
sender: borrower,
collateralAddress: state.borrowingAssetAddress,
borrowAddress: state.collateralAssetAddress
});
@ -284,10 +447,10 @@ contract CrossChainBorrowLend is
);
} else {
// save the total normalized borrow amount for repayments
state.totalAssets.borrowed +=
state.totalAssets.source.borrowed +=
params.totalNormalizedBorrowAmount -
state.accountAssets[params.header.borrower].sourceBorrowed;
state.accountAssets[params.header.borrower].sourceBorrowed = params
state.accountAssets[borrower].source.borrowed;
state.accountAssets[borrower].source.borrowed = params
.totalNormalizedBorrowAmount;
// params.borrowAmount == 0 means that there was a repayment
@ -299,7 +462,7 @@ contract CrossChainBorrowLend is
SafeERC20.safeTransferFrom(
collateralToken(),
address(this),
params.header.borrower,
borrower,
params.borrowAmount
);
}
@ -345,9 +508,10 @@ contract CrossChainBorrowLend is
params.sourceInterestAccrualIndex
);
state
.accountAssets[params.header.borrower]
.targetBorrowed -= normalizedAmount;
state.totalAssets.borrowed -= normalizedAmount;
.accountAssets[params.header.sender]
.target
.borrowed -= normalizedAmount;
state.totalAssets.target.borrowed -= normalizedAmount;
}
function initiateRepay(uint256 amount)
@ -360,28 +524,26 @@ contract CrossChainBorrowLend is
// 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()
];
SourceTargetUints memory account = state.accountAssets[_msgSender()];
// update the index
updateInterestAccrualIndex();
updateSourceInterestAccrualIndex();
// cache the index to save gas
uint256 index = borrowedInterestAccrualIndex();
uint256 borrowedIndex = sourceBorrowedInterestAccrualIndex();
// save the normalized amount
uint256 normalizedAmount = normalizeAmount(amount, index);
uint256 normalizedAmount = normalizeAmount(amount, borrowedIndex);
// confirm that the caller has loans to pay back
require(
normalizedAmount <= normalizedAmounts.sourceBorrowed,
normalizedAmount <= account.source.borrowed,
"loan payment too large"
);
// update state on this contract
state.accountAssets[_msgSender()].sourceBorrowed -= normalizedAmount;
state.totalAssets.borrowed -= normalizedAmount;
state.accountAssets[_msgSender()].source.borrowed -= normalizedAmount;
state.totalAssets.source.borrowed -= normalizedAmount;
// transfer to this contract
SafeERC20.safeTransferFrom(
@ -394,7 +556,7 @@ contract CrossChainBorrowLend is
// construct wormhole message
MessageHeader memory header = MessageHeader({
payloadID: uint8(3),
borrower: _msgSender(),
sender: _msgSender(),
collateralAddress: state.borrowingAssetAddress,
borrowAddress: state.collateralAssetAddress
});
@ -405,7 +567,7 @@ contract CrossChainBorrowLend is
RepayMessage({
header: header,
repayAmount: amount,
targetInterestAccrualIndex: index,
targetInterestAccrualIndex: borrowedIndex,
repayTimestamp: block.timestamp,
paidInFull: 0
})
@ -421,33 +583,31 @@ contract CrossChainBorrowLend is
// 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()
];
SourceTargetUints memory account = state.accountAssets[_msgSender()];
// update the index
updateInterestAccrualIndex();
updateSourceInterestAccrualIndex();
// cache the index to save gas
uint256 index = borrowedInterestAccrualIndex();
uint256 borrowedIndex = sourceBorrowedInterestAccrualIndex();
// update state on the contract
uint256 normalizedAmount = normalizedAmounts.sourceBorrowed;
state.accountAssets[_msgSender()].sourceBorrowed = 0;
state.totalAssets.borrowed -= normalizedAmount;
uint256 normalizedAmount = account.source.borrowed;
state.accountAssets[_msgSender()].source.borrowed = 0;
state.totalAssets.source.borrowed -= normalizedAmount;
// transfer to this contract
SafeERC20.safeTransferFrom(
borrowToken(),
_msgSender(),
address(this),
denormalizeAmount(normalizedAmount, index)
denormalizeAmount(normalizedAmount, borrowedIndex)
);
// construct wormhole message
MessageHeader memory header = MessageHeader({
payloadID: uint8(3),
borrower: _msgSender(),
sender: _msgSender(),
collateralAddress: state.borrowingAssetAddress,
borrowAddress: state.collateralAssetAddress
});
@ -457,8 +617,11 @@ contract CrossChainBorrowLend is
encodeRepayMessage(
RepayMessage({
header: header,
repayAmount: denormalizeAmount(normalizedAmount, index),
targetInterestAccrualIndex: index,
repayAmount: denormalizeAmount(
normalizedAmount,
borrowedIndex
),
targetInterestAccrualIndex: borrowedIndex,
repayTimestamp: block.timestamp,
paidInFull: 1
})
@ -486,13 +649,14 @@ contract CrossChainBorrowLend is
consumeMessageHash(parsed.hash);
// update the index
updateInterestAccrualIndex();
updateTargetInterestAccrualIndex();
// cache the index to save gas
uint256 index = borrowedInterestAccrualIndex();
uint256 borrowedIndex = targetBorrowedInterestAccrualIndex();
// decode the RepayMessage
RepayMessage memory params = decodeRepayMessage(parsed.payload);
address borrower = params.header.sender;
// correct assets?
require(verifyAssetMetaFromRepay(params), "invalid asset metadata");
@ -509,17 +673,18 @@ contract CrossChainBorrowLend is
params.repayAmount,
params.targetInterestAccrualIndex
);
state.accountAssets[params.header.borrower].targetBorrowed = 0;
state.totalAssets.borrowed -= normalizedAmount;
state.accountAssets[borrower].target.borrowed = 0;
state.totalAssets.target.borrowed -= normalizedAmount;
} else {
uint256 normalizedAmount = normalizeAmount(
params.repayAmount,
index
borrowedIndex
);
state
.accountAssets[params.header.borrower]
.targetBorrowed -= normalizedAmount;
state.totalAssets.borrowed -= normalizedAmount;
.accountAssets[borrower]
.target
.borrowed -= normalizedAmount;
state.totalAssets.target.borrowed -= normalizedAmount;
// Send a wormhole message again since he did not repay in full
// (due to repaying outside of the grace period)
@ -528,15 +693,16 @@ contract CrossChainBorrowLend is
BorrowMessage({
header: MessageHeader({
payloadID: uint8(1),
borrower: params.header.borrower,
sender: 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
.accountAssets[borrower]
.target
.borrowed,
interestAccrualIndex: borrowedIndex
})
)
);
@ -547,10 +713,8 @@ contract CrossChainBorrowLend is
params.repayAmount,
params.targetInterestAccrualIndex
);
state
.accountAssets[params.header.borrower]
.targetBorrowed -= normalizedAmount;
state.totalAssets.borrowed -= normalizedAmount;
state.accountAssets[borrower].target.borrowed -= normalizedAmount;
state.totalAssets.target.borrowed -= normalizedAmount;
}
}

View File

@ -53,29 +53,46 @@ contract CrossChainBorrowLendGetters is Context, CrossChainBorrowLendState {
);
}
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 sourceCollateralInterestAccrualIndex()
public
view
returns (uint256)
{
return state.interestAccrualIndex.source.deposited;
}
function borrowedInterestAccrualIndex() public view returns (uint256) {
return state.interestAccrualIndex;
function targetCollateralInterestAccrualIndex()
public
view
returns (uint256)
{
return state.interestAccrualIndex.target.deposited;
}
function sourceBorrowedInterestAccrualIndex()
public
view
returns (uint256)
{
return state.interestAccrualIndex.source.borrowed;
}
function targetBorrowedInterestAccrualIndex()
public
view
returns (uint256)
{
return state.interestAccrualIndex.target.borrowed;
}
function mockPyth() internal view returns (IMockPyth) {
return IMockPyth(state.mockPythAddress);
}
function normalizedLiquidity() internal view returns (uint256) {
return state.totalAssets.deposited - state.totalAssets.borrowed;
function sourceLiquidity() internal view returns (uint256) {
return
state.totalAssets.source.deposited -
state.totalAssets.source.borrowed;
}
function denormalizeAmount(
@ -103,7 +120,7 @@ contract CrossChainBorrowLendGetters is Context, CrossChainBorrowLendState {
function normalizedAmounts()
public
view
returns (NormalizedTotalAmounts memory)
returns (SourceTargetUints memory)
{
return state.totalAssets;
}
@ -116,16 +133,16 @@ contract CrossChainBorrowLendGetters is Context, CrossChainBorrowLendState {
// 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];
SourceTargetUints memory normalized = state.accountAssets[account];
// denormalize
uint256 denormalizedDeposited = denormalizeAmount(
normalized.sourceDeposited,
collateralInterestAccrualIndex()
normalized.source.deposited,
sourceCollateralInterestAccrualIndex()
);
uint256 denormalizedBorrowed = denormalizeAmount(
normalized.targetBorrowed,
borrowedInterestAccrualIndex()
normalized.target.borrowed,
targetBorrowedInterestAccrualIndex()
);
return
@ -158,16 +175,16 @@ contract CrossChainBorrowLendGetters is Context, CrossChainBorrowLendState {
// 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];
SourceTargetUints memory normalized = state.accountAssets[account];
// denormalize
uint256 denormalizedDeposited = denormalizeAmount(
normalized.sourceDeposited,
collateralInterestAccrualIndex()
normalized.source.deposited,
sourceCollateralInterestAccrualIndex()
);
uint256 denormalizedBorrowed = denormalizeAmount(
normalized.targetBorrowed,
borrowedInterestAccrualIndex()
normalized.target.borrowed,
targetBorrowedInterestAccrualIndex()
);
return

View File

@ -16,7 +16,7 @@ contract CrossChainBorrowLendMessages {
{
return
abi.encodePacked(
header.borrower,
header.sender,
header.collateralAddress,
header.borrowAddress
);
@ -77,6 +77,20 @@ contract CrossChainBorrowLendMessages {
);
}
function encodeDepositChangeMessage(DepositChangeMessage memory message)
internal
pure
returns (bytes memory)
{
return
abi.encodePacked(
uint8(5), // payloadID
encodeMessageHeader(message.header),
uint8(message.depositType),
message.amount
);
}
function decodeMessageHeader(bytes memory serialized)
internal
pure
@ -86,7 +100,7 @@ contract CrossChainBorrowLendMessages {
// parse the header
header.payloadID = serialized.toUint8(index += 1);
header.borrower = serialized.toAddress(index += 20);
header.sender = serialized.toAddress(index += 20);
header.collateralAddress = serialized.toAddress(index += 20);
header.borrowAddress = serialized.toAddress(index += 20);
}
@ -165,4 +179,34 @@ contract CrossChainBorrowLendMessages {
require(params.header.payloadID == 4, "invalid message");
require(index == serialized.length, "index != serialized.length");
}
function decodeDepositChangeMessage(bytes memory serialized)
internal
pure
returns (DepositChangeMessage memory params)
{
uint256 index = 0;
// parse the message header
params.header = decodeMessageHeader(
serialized.slice(index, index += 61)
);
// handle DepositType enum value
uint8 depositTypeValue = serialized.toUint8(index += 1);
if (depositTypeValue == uint8(DepositType.Add)) {
params.depositType = DepositType.Add;
} else if (depositTypeValue == uint8(DepositType.Remove)) {
params.depositType = DepositType.Remove;
} else if (depositTypeValue == uint8(DepositType.RemoveFull)) {
params.depositType = DepositType.RemoveFull;
}
else {
revert("unrecognized deposit type");
}
params.amount = serialized.toUint256(index += 32);
require(params.header.payloadID == 5, "invalid message");
require(index == serialized.length, "index != serialized.length");
}
}

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {NormalizedAmounts, NormalizedTotalAmounts, InterestRateModel} from "./CrossChainBorrowLendStructs.sol";
import "./CrossChainBorrowLendStructs.sol";
contract CrossChainBorrowLendStorage {
struct State {
@ -20,12 +20,12 @@ contract CrossChainBorrowLendStorage {
bytes32 collateralAssetPythId;
uint256 collateralizationRatio;
address borrowingAssetAddress;
uint256 interestAccrualIndex;
SourceTargetUints interestAccrualIndex;
uint256 interestAccrualIndexPrecision;
uint256 lastActivityBlockTimestamp;
NormalizedTotalAmounts totalAssets;
SourceTargetUints totalAssets;
uint256 repayGracePeriod;
mapping(address => NormalizedAmounts) accountAssets;
mapping(address => SourceTargetUints) accountAssets;
bytes32 borrowingAssetPythId;
mapping(bytes32 => bool) consumedMessages;
InterestRateModel interestRateModel;

View File

@ -1,27 +1,27 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
enum DepositType {
None,
Add,
Remove,
RemoveFull
}
struct DepositedBorrowedUints {
uint256 deposited;
uint256 borrowed;
}
struct NormalizedTotalAmounts {
uint256 deposited;
uint256 borrowed;
}
struct NormalizedAmounts {
uint256 sourceDeposited;
uint256 sourceBorrowed;
uint256 targetDeposited;
uint256 targetBorrowed;
struct SourceTargetUints {
DepositedBorrowedUints source;
DepositedBorrowedUints target;
}
struct MessageHeader {
uint8 payloadID;
// address of the borrower
address borrower;
// address of the sender
address sender;
// collateral info
address collateralAddress; // for verification
// borrow info
@ -58,9 +58,17 @@ struct LiquidationIntentMessage {
// TODO: add necessary variables
}
struct DepositChangeMessage {
// payloadID = 5
MessageHeader header;
DepositType depositType;
uint256 amount;
}
struct InterestRateModel {
uint64 ratePrecision;
uint64 rateIntercept;
uint64 rateCoefficientA;
// TODO: add more complexity for example?
uint64 reserveFactor;
}

View File

@ -3,7 +3,7 @@
pragma solidity ^0.8.0;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {NormalizedAmounts, NormalizedTotalAmounts} from "../src/CrossChainBorrowLendStructs.sol";
import "../src/CrossChainBorrowLendStructs.sol";
import {ExposedCrossChainBorrowLend} from "./helpers/ExposedCrossChainBorrowLend.sol";
import {MyERC20} from "./helpers/MyERC20.sol";
import "forge-std/Test.sol";
@ -46,7 +46,7 @@ contract CrossChainBorrowLendTest is Test {
);
}
function testComputeInterestProportion() public {
function testSourceComputeInterestFactor() public {
// start from zero
vm.warp(0);
uint256 timeStart = block.timestamp;
@ -59,75 +59,125 @@ contract CrossChainBorrowLendTest is Test {
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
);
// set state for test
uint256 sourceDeposited = 1e18; // 1 WBNB (18 decimals)
uint256 sourceBorrowed = 0.5e18; // 0.5 WBNB (18 decimals)
borrowLendContract.HACKED_setAccountAssets(
msg.sender,
sourceDeposited,
sourceBorrowed,
0, // targetDeposited
0 // targetBorrowed
);
// expect using the correct value (0.0205e18)
{
require(
interestProportion == 0.0205e18,
"interestProportion != expected"
borrowLendContract.EXPOSED_computeSourceInterestFactor(
secondsElapsed,
intercept,
coefficient
) == 0.0205e18,
"computeSourceInterestFactor(...) != expected"
);
}
// expect using calculation
{
uint256 expected = intercept + (coefficient * borrowed) / deposited;
require(
interestProportion == expected,
"interestProportion != expected (computed)"
borrowLendContract.EXPOSED_computeTargetInterestFactor(
secondsElapsed,
intercept,
coefficient
) == 0,
"computeTargetInterestFactor(...) != expected"
);
}
// clear
borrowLendContract.HACKED_setTotalAssetsDeposited(0);
borrowLendContract.HACKED_setTotalAssetsBorrowed(0);
borrowLendContract.HACKED_resetAccountAssets(msg.sender);
}
function testUpdateInterestAccrualIndex() public {
function testTargetComputeInterestFactor() 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
// set state for test
uint256 targetDeposited = 100e6; // 100 USDC (6 decimals)
uint256 targetBorrowed = 50e6; // 50 USDC (6 decimals)
borrowLendContract.HACKED_setAccountAssets(
msg.sender,
0, // sourceDeposited
0, // sourceBorrowed
targetDeposited,
targetBorrowed
);
// expect using the correct value (0.0205e18)
{
require(
borrowLendContract.EXPOSED_computeSourceInterestFactor(
secondsElapsed,
intercept,
coefficient
) == 0,
"computeSourceInterestFactor(...) != expected"
);
require(
borrowLendContract.EXPOSED_computeTargetInterestFactor(
secondsElapsed,
intercept,
coefficient
) == 0.0205e18,
"computeTargetInterestFactor(...) != expected"
);
}
// clear
borrowLendContract.HACKED_resetAccountAssets(msg.sender);
}
function testUpdateSourceInterestAccrualIndex() 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);
// set state for test
uint256 sourceDeposited = 200e18; // 200 WBNB (18 decimals)
uint256 sourceBorrowed = 20e18; // 20 WBNB (18 decimals)
borrowLendContract.HACKED_setAccountAssets(
msg.sender,
sourceDeposited,
sourceBorrowed,
0, // targetDeposited
0 // targetBorrowed
);
// warp to 1 year in the future
vm.warp(365 * 24 * 60 * 60);
// trigger accrual
borrowLendContract.EXPOSED_updateInterestAccrualIndex();
borrowLendContract.EXPOSED_updateSourceInterestAccrualIndex();
{
// expect using the correct value (1.02e18)
require(
borrowLendContract.borrowedInterestAccrualIndex() == 1.02e18,
"borrowedInterestAccrualIndex() != expected (first iteration)"
borrowLendContract.sourceBorrowedInterestAccrualIndex() ==
1.02e18,
"sourceBorrowedInterestAccrualIndex() != expected (first iteration)"
);
// expect using the correct value (1.002e18)
require(
borrowLendContract.collateralInterestAccrualIndex() == 1.002e18,
"collateralInterestAccrualIndex() != expected (first iteration)"
borrowLendContract.sourceCollateralInterestAccrualIndex() ==
1.002e18,
"sourceBollateralInterestAccrualIndex() != expected (first iteration)"
);
}
@ -135,35 +185,38 @@ contract CrossChainBorrowLendTest is Test {
vm.warp(2 * 365 * 24 * 60 * 60);
// trigger accrual again
borrowLendContract.EXPOSED_updateInterestAccrualIndex();
borrowLendContract.EXPOSED_updateSourceInterestAccrualIndex();
{
// expect using the correct value (1.04e18)
require(
borrowLendContract.borrowedInterestAccrualIndex() == 1.04e18,
"borrowedInterestAccrualIndex() != expected (second iteration)"
borrowLendContract.sourceBorrowedInterestAccrualIndex() ==
1.04e18,
"sourceBorrowedInterestAccrualIndex() != expected (second iteration)"
);
// expect using the correct value (1.004e18)
require(
borrowLendContract.collateralInterestAccrualIndex() == 1.004e18,
"collateralInterestAccrualIndex() != expected (second iteration)"
borrowLendContract.sourceCollateralInterestAccrualIndex() ==
1.004e18,
"sourceCollateralInterestAccrualIndex() != expected (second iteration)"
);
}
// check denormalized deposit and borrowed. should be equal
{
NormalizedTotalAmounts memory amounts = borrowLendContract
.normalizedAmounts();
DepositedBorrowedUints memory amounts = borrowLendContract
.normalizedAmounts()
.source;
uint256 accruedDepositedInterest = borrowLendContract
.denormalizeAmount(
amounts.deposited,
borrowLendContract.collateralInterestAccrualIndex()
) - deposited;
borrowLendContract.sourceCollateralInterestAccrualIndex()
) - sourceDeposited;
uint256 accruedBorrowedInterest = borrowLendContract
.denormalizeAmount(
amounts.borrowed,
borrowLendContract.borrowedInterestAccrualIndex()
) - borrowed;
borrowLendContract.sourceBorrowedInterestAccrualIndex()
) - sourceBorrowed;
require(
accruedDepositedInterest == accruedBorrowedInterest,
"accruedDepositedInterest != accruedBorrowedInterest"
@ -171,24 +224,106 @@ contract CrossChainBorrowLendTest is Test {
}
// clear
borrowLendContract.HACKED_setTotalAssetsDeposited(0);
borrowLendContract.HACKED_setTotalAssetsBorrowed(0);
borrowLendContract.HACKED_resetAccountAssets(msg.sender);
}
function testUpdateTargetInterestAccrualIndex() public {
// start from zero
vm.warp(0);
borrowLendContract.HACKED_setLastActivityBlockTimestamp(
block.timestamp
);
// set state for test
uint256 targetDeposited = 200e6; // 200 USDC (6 decimals)
uint256 targetBorrowed = 20e6; // 20 USDC (6 decimals)
borrowLendContract.HACKED_setAccountAssets(
msg.sender,
0, // sourceDeposited
0, // sourceBorrowed
targetDeposited,
targetBorrowed
);
// warp to 1 year in the future
vm.warp(365 * 24 * 60 * 60);
// trigger accrual
borrowLendContract.EXPOSED_updateTargetInterestAccrualIndex();
{
// expect using the correct value (1.02e18)
require(
borrowLendContract.targetBorrowedInterestAccrualIndex() ==
1.02e18,
"targetBorrowedInterestAccrualIndex() != expected (first iteration)"
);
// expect using the correct value (1.002e18)
require(
borrowLendContract.targetCollateralInterestAccrualIndex() ==
1.002e18,
"targetCollateralInterestAccrualIndex() != expected (first iteration)"
);
}
// warp to 2 years in the future
vm.warp(2 * 365 * 24 * 60 * 60);
// trigger accrual again
borrowLendContract.EXPOSED_updateTargetInterestAccrualIndex();
{
// expect using the correct value (1.04e18)
require(
borrowLendContract.targetBorrowedInterestAccrualIndex() ==
1.04e18,
"targetBorrowedInterestAccrualIndex() != expected (second iteration)"
);
// expect using the correct value (1.004e18)
require(
borrowLendContract.targetCollateralInterestAccrualIndex() ==
1.004e18,
"targetCollateralInterestAccrualIndex() != expected (second iteration)"
);
}
// check denormalized deposit and borrowed. should be equal
{
DepositedBorrowedUints memory amounts = borrowLendContract
.normalizedAmounts()
.target;
uint256 accruedDepositedInterest = borrowLendContract
.denormalizeAmount(
amounts.deposited,
borrowLendContract.sourceCollateralInterestAccrualIndex()
) - targetDeposited;
uint256 accruedBorrowedInterest = borrowLendContract
.denormalizeAmount(
amounts.borrowed,
borrowLendContract.sourceBorrowedInterestAccrualIndex()
) - targetBorrowed;
require(
accruedDepositedInterest == accruedBorrowedInterest,
"accruedDepositedInterest != accruedBorrowedInterest"
);
}
// clear
borrowLendContract.HACKED_resetAccountAssets(msg.sender);
}
function testMaxAllowedToWithdraw() public {
uint64 collateralPrice = 400; // WBNB
uint64 borrowAssetPrice = 1; // USDC
uint256 deposited = 1e18; // 1 WBNB (18 decimals)
borrowLendContract.HACKED_setAccountAssetsDeposited(
uint256 sourceDeposited = 1e18; // 1 WBNB (18 decimals)
uint256 targetBorrowed = 100e6; // 100 USDC (6 decimals)
borrowLendContract.HACKED_setAccountAssets(
msg.sender,
deposited
);
uint256 borrowed = 100e6; // 100 USDC (6 decimals)
borrowLendContract.HACKED_setAccountAssetsBorrowed(
msg.sender,
borrowed
sourceDeposited,
0, // sourceBorrowed
0, // targetDeposited
targetBorrowed
);
uint256 maxAllowed = borrowLendContract
@ -204,24 +339,21 @@ contract CrossChainBorrowLendTest is Test {
}
// clear
borrowLendContract.HACKED_setAccountAssetsDeposited(msg.sender, 0);
borrowLendContract.HACKED_setAccountAssetsBorrowed(msg.sender, 0);
borrowLendContract.HACKED_resetAccountAssets(msg.sender);
}
function testMaxAllowedToBorrow() public {
uint64 collateralPrice = 400; // WBNB
uint64 borrowAssetPrice = 1; // USDC
uint256 deposited = 1e18; // 1 WBNB (18 decimals)
borrowLendContract.HACKED_setAccountAssetsDeposited(
uint256 sourceDeposited = 1e18; // 1 WBNB (18 decimals)
uint256 targetBorrowed = 100e6; // 100 USDC (6 decimals)
borrowLendContract.HACKED_setAccountAssets(
msg.sender,
deposited
);
uint256 borrowed = 100e6; // 100 USDC (6 decimals)
borrowLendContract.HACKED_setAccountAssetsBorrowed(
msg.sender,
borrowed
sourceDeposited,
0, // sourceBorrowed
0, // targetDeposited
targetBorrowed
);
uint256 maxAllowed = borrowLendContract
@ -237,7 +369,6 @@ contract CrossChainBorrowLendTest is Test {
}
// clear
borrowLendContract.HACKED_setAccountAssetsDeposited(msg.sender, 0);
borrowLendContract.HACKED_setAccountAssetsBorrowed(msg.sender, 0);
borrowLendContract.HACKED_resetAccountAssets(msg.sender);
}
}

View File

@ -2,7 +2,7 @@
pragma solidity ^0.8.0;
import {NormalizedAmounts} from "../../src/CrossChainBorrowLendStructs.sol";
import "../../src/CrossChainBorrowLendStructs.sol";
import {CrossChainBorrowLend} from "../../src/CrossChainBorrowLend.sol";
import "forge-std/Test.sol";
@ -39,17 +39,30 @@ contract ExposedCrossChainBorrowLend is CrossChainBorrowLend {
// nothing else
}
function EXPOSED_computeInterestProportion(
function EXPOSED_computeSourceInterestFactor(
uint256 secondsElapsed,
uint256 intercept,
uint256 coefficient
) external view returns (uint256) {
return
computeInterestProportion(secondsElapsed, intercept, coefficient);
computeSourceInterestFactor(secondsElapsed, intercept, coefficient);
}
function EXPOSED_updateInterestAccrualIndex() external {
return updateInterestAccrualIndex();
function EXPOSED_computeTargetInterestFactor(
uint256 secondsElapsed,
uint256 intercept,
uint256 coefficient
) external view returns (uint256) {
return
computeTargetInterestFactor(secondsElapsed, intercept, coefficient);
}
function EXPOSED_updateSourceInterestAccrualIndex() external {
return updateSourceInterestAccrualIndex();
}
function EXPOSED_updateTargetInterestAccrualIndex() external {
return updateTargetInterestAccrualIndex();
}
function EXPOSED_maxAllowedToBorrowWithPrices(
@ -81,32 +94,35 @@ contract ExposedCrossChainBorrowLend is CrossChainBorrowLend {
function EXPOSED_accountAssets(address account)
external
view
returns (NormalizedAmounts memory)
returns (SourceTargetUints memory)
{
return state.accountAssets[account];
}
function HACKED_setTotalAssetsDeposited(uint256 amount) external {
state.totalAssets.deposited = amount;
function HACKED_setAccountAssets(
address account,
uint256 sourceDeposited,
uint256 sourceBorrowed,
uint256 targetDeposited,
uint256 targetBorrowed
) public {
// account
state.accountAssets[account].source.deposited = sourceDeposited;
state.accountAssets[account].source.borrowed = sourceBorrowed;
state.accountAssets[account].target.deposited = targetDeposited;
state.accountAssets[account].target.borrowed = targetBorrowed;
// total
state.totalAssets.source.deposited = sourceDeposited;
state.totalAssets.source.borrowed = sourceBorrowed;
state.totalAssets.target.deposited = targetDeposited;
state.totalAssets.target.borrowed = targetBorrowed;
}
function HACKED_setTotalAssetsBorrowed(uint256 amount) external {
state.totalAssets.borrowed = amount;
function HACKED_resetAccountAssets(address account) public {
HACKED_setAccountAssets(account, 0, 0, 0, 0);
}
function HACKED_setLastActivityBlockTimestamp(uint256 timestamp) external {
function HACKED_setLastActivityBlockTimestamp(uint256 timestamp) public {
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;
}
}