diff --git a/src/hub/Accounting.sol b/src/hub/Accounting.sol index 618f2f6a2..f50350652 100644 --- a/src/hub/Accounting.sol +++ b/src/hub/Accounting.sol @@ -10,8 +10,8 @@ import {AccountId} from "src/common/types/AccountId.sol"; import {IAccounting, JournalEntry} from "src/hub/interfaces/IAccounting.sol"; /// @notice In a transaction there can be multiple journal entries for different pools, -/// which can be interleaved. We want entries for the same pool to share the same journal ID. -/// So we're keeping a journal ID per pool in transient storage. +/// which can be interleaved. We want entries for the same pool to share the same journal ID. +/// So we're keeping a journal ID per pool in transient storage. library TransientJournal { function journalId(PoolId poolId) internal view returns (uint256) { return TransientStorageLib.tloadUint256(keccak256(abi.encode("journalId", poolId))); diff --git a/src/managers/LoansManager.sol b/src/managers/LoansManager.sol new file mode 100644 index 000000000..af0a05d29 --- /dev/null +++ b/src/managers/LoansManager.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {Auth} from "src/misc/Auth.sol"; +import {IERC6909NFT} from "src/misc/interfaces/IERC6909.sol"; +import {D18, d18} from "src/misc/types/D18.sol"; +import {ILinearAccrual} from "src/misc/interfaces/ILinearAccrual.sol"; +import {SafeTransferLib} from "src/misc/libraries/SafeTransferLib.sol"; + +import {IValuation} from "src/common/interfaces/IValuation.sol"; +import {UpdateContractMessageLib, UpdateContractType} from "src/spoke/libraries/UpdateContractMessageLib.sol"; +import {PoolId} from "src/common/types/PoolId.sol"; +import {AssetId} from "src/common/types/AssetId.sol"; +import {AccountId} from "src/common/types/AccountId.sol"; +import {ShareClassId} from "src/common/types/ShareClassId.sol"; + +import {ISpoke} from "src/spoke/interfaces/ISpoke.sol"; +import {IBalanceSheet} from "src/spoke/interfaces/IBalanceSheet.sol"; + +struct Loan { + // System properties + ShareClassId scId; + uint16 tokenId; + address borrower; + // Loan properties + address borrowAsset; + bytes32 rateId; + D18 maxBorrowAmount; + // Ongoing properties + int128 normalizedDebt; + D18 totalBorrowed; + D18 totalRepaid; +} + +// TODO: maturity date and/or open term + +contract LoansManager is Auth, IValuation { + error UnknownUpdateContractType(); + error UnregisteredRateId(); + error TooManyLoans(); + error NotTheOwner(); + error NonZeroOutstanding(); + error ExceedsLTV(); + + PoolId public immutable poolId; + AccountId public immutable equityAccount; + AccountId public immutable lossAccount; + AccountId public immutable gainAccount; + + IERC6909NFT public immutable token; + ILinearAccrual public immutable linearAccrual; + + ISpoke public spoke; + IBalanceSheet public balanceSheet; + + mapping(AssetId assetId => Loan) public loans; + + constructor( + PoolId poolId_, + IERC6909NFT token_, + ILinearAccrual linearAccrual_, + ISpoke spoke_, + IBalanceSheet balanceSheet_, + AccountId equityAccount_, + AccountId lossAccount_, + AccountId gainAccount_, + address deployer + ) Auth(deployer) { + poolId = poolId_; + equityAccount = equityAccount_; + lossAccount = lossAccount_; + gainAccount = gainAccount_; + + token = token_; + linearAccrual = linearAccrual_; + + spoke = spoke_; + balanceSheet = balanceSheet_; + } + + //---------------------------------------------------------------------------------------------- + // Owner actions + //---------------------------------------------------------------------------------------------- + + function update(PoolId, /* poolId */ ShareClassId, /* scId */ bytes calldata payload) external auth { + uint8 kind = uint8(UpdateContractMessageLib.updateContractType(payload)); + + if (kind == uint8(UpdateContractType.LoanMaxBorrowAmount)) { + UpdateContractMessageLib.UpdateContractLoanMaxBorrowAmount memory m = + UpdateContractMessageLib.deserializeUpdateContractLoanMaxBorrowAmount(payload); + + Loan storage loan = loans[AssetId.wrap(m.assetId)]; + require(linearAccrual.debt(loan.rateId, loan.normalizedDebt) <= int128(m.maxBorrowAmount), ExceedsLTV()); + + loan.maxBorrowAmount = d18(m.maxBorrowAmount); + // emit UpdateLoanMaxBorrowAmount(); + } else if (kind == uint8(UpdateContractType.LoanRate)) { + UpdateContractMessageLib.UpdateContractLoanRate memory m = + UpdateContractMessageLib.deserializeUpdateContractLoanRate(payload); + + Loan storage loan = loans[AssetId.wrap(m.assetId)]; + + loan.normalizedDebt = linearAccrual.getRenormalizedDebt(loan.rateId, m.rateId, loan.normalizedDebt); + loan.rateId = m.rateId; + // emit UpdateLoanRate(); + } else { + revert UnknownUpdateContractType(); + } + } + + //---------------------------------------------------------------------------------------------- + // Borrower actions + //---------------------------------------------------------------------------------------------- + + function create(ShareClassId scId, address borrower, address borrowAsset, bytes32 rateId, string memory tokenURI) + external + { + require(linearAccrual.rateIdExists(rateId), UnregisteredRateId()); + + uint256 tokenId = token.mint(address(this), tokenURI); + require(tokenId <= type(uint16).max, TooManyLoans()); + AssetId assetId = spoke.registerAsset(poolId.centrifugeId(), address(this), tokenId); + + loans[assetId] = Loan({ + scId: scId, + tokenId: uint16(tokenId), + borrower: borrower, + borrowAsset: borrowAsset, + rateId: rateId, + maxBorrowAmount: d18(0), + normalizedDebt: 0, + totalBorrowed: d18(0), + totalRepaid: d18(0) + }); + + balanceSheet.deposit(poolId, scId, address(this), uint16(tokenId), 1); + + // emit NewLoan(..); + } + + function borrow(AssetId assetId, uint128 amount, address receiver) external { + Loan storage loan = loans[assetId]; + require(loan.borrower == msg.sender, NotTheOwner()); + require( + linearAccrual.debt(loan.rateId, loan.normalizedDebt) + int128(amount) <= int128(loan.maxBorrowAmount.raw()), + ExceedsLTV() + ); + + loan.normalizedDebt = linearAccrual.getModifiedNormalizedDebt(loan.rateId, loan.normalizedDebt, int128(amount)); + loan.totalBorrowed = loan.totalBorrowed + d18(amount); + + balanceSheet.withdraw(poolId, loan.scId, loan.borrowAsset, loan.tokenId, receiver, amount); + } + + function repay(AssetId assetId, uint128 amount, address owner) external { + Loan storage loan = loans[assetId]; + require(loan.borrower == msg.sender, NotTheOwner()); + + loan.normalizedDebt = linearAccrual.getModifiedNormalizedDebt(loan.rateId, loan.normalizedDebt, -int128(amount)); + loan.totalRepaid = loan.totalRepaid + d18(amount); + + SafeTransferLib.safeTransferFrom(loan.borrowAsset, owner, address(this), amount); + balanceSheet.deposit(poolId, loan.scId, loan.borrowAsset, loan.tokenId, amount); + } + + function close(AssetId assetId) external { + Loan storage loan = loans[assetId]; + require(loan.borrower == msg.sender, NotTheOwner()); + require(loan.normalizedDebt == 0, NonZeroOutstanding()); + + balanceSheet.withdraw(poolId, loan.scId, address(this), loan.tokenId, address(this), 1); + token.burn(loan.tokenId); + } + + //---------------------------------------------------------------------------------------------- + // Valuation + //---------------------------------------------------------------------------------------------- + + function getQuote(uint128, AssetId base, AssetId /* quote */ ) external view returns (uint128 quoteAmount) { + // TODO: how to know conversion to quote asset? + + Loan storage loan = loans[base]; + int128 debt = linearAccrual.debt(loan.rateId, loan.normalizedDebt); + quoteAmount = debt > 0 ? uint128(debt) : 0; + } +} diff --git a/src/misc/ERC6909.sol b/src/misc/ERC6909.sol new file mode 100644 index 000000000..fc7f55423 --- /dev/null +++ b/src/misc/ERC6909.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {IERC165} from "forge-std/interfaces/IERC165.sol"; + +import {IERC6909} from "src/misc/interfaces/IERC6909.sol"; + +/// @title Basic implementation of all properties according to the ERC6909. +/// +/// @dev This implementation MUST be extended with another contract which defines how tokens are created. +/// Either implement mint/burn or override transfer/transferFrom. +abstract contract ERC6909 is IERC6909 { + mapping(address owner => mapping(uint256 tokenId => uint256)) public balanceOf; + mapping(address owner => mapping(address operator => bool)) public isOperator; + mapping(address owner => mapping(address spender => mapping(uint256 tokenId => uint256))) public allowance; + + /// @inheritdoc IERC6909 + function transfer(address receiver, uint256 tokenId, uint256 amount) external virtual returns (bool) { + return _transfer(msg.sender, receiver, tokenId, amount); + } + + /// @inheritdoc IERC6909 + function transferFrom(address sender, address receiver, uint256 tokenId, uint256 amount) + external + virtual + returns (bool) + { + return _transferFrom(msg.sender, sender, receiver, tokenId, amount); + } + + /// @inheritdoc IERC6909 + function approve(address spender, uint256 tokenId, uint256 amount) external returns (bool) { + allowance[msg.sender][spender][tokenId] = amount; + + emit Approval(msg.sender, spender, tokenId, amount); + + return true; + } + + /// @inheritdoc IERC6909 + function setOperator(address operator, bool approved) external returns (bool) { + isOperator[msg.sender][operator] = approved; + + emit OperatorSet(msg.sender, operator, approved); + + return true; + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public pure virtual returns (bool) { + return type(IERC6909).interfaceId == interfaceId || type(IERC165).interfaceId == interfaceId; + } + + function _mint(address owner, uint256 tokenId, uint256 amount) internal { + require(owner != address(0), EmptyOwner()); + require(tokenId > 0, InvalidTokenId()); + require(amount > 0, EmptyAmount()); + + balanceOf[owner][tokenId] += amount; + + emit Transfer(msg.sender, address(0), owner, tokenId, amount); + } + + function _burn(address owner, uint256 tokenId, uint256 amount) internal { + uint256 balance = balanceOf[owner][tokenId]; + require(balance >= amount, InsufficientBalance(msg.sender, tokenId)); + + // The underflow check is handled by the require line above + unchecked { + balanceOf[owner][tokenId] -= amount; + } + + emit Transfer(owner, owner, address(0), tokenId, amount); + } + + function _transferFrom(address spender, address sender, address receiver, uint256 tokenId, uint256 amount) + internal + returns (bool) + { + if (spender != sender && !isOperator[sender][spender]) { + uint256 allowed = allowance[sender][spender][tokenId]; + if (allowed != type(uint256).max) { + require(amount <= allowed, InsufficientAllowance(spender, tokenId)); + allowance[sender][spender][tokenId] -= amount; + } + } + + return _transfer(sender, receiver, tokenId, amount); + } + + function _transfer(address sender, address receiver, uint256 tokenId, uint256 amount) internal returns (bool) { + uint256 senderBalance = balanceOf[sender][tokenId]; + require(senderBalance >= amount, InsufficientBalance(sender, tokenId)); + + // The require check few lines above guarantees that + // it cannot underflow. + unchecked { + balanceOf[sender][tokenId] -= amount; + } + + balanceOf[receiver][tokenId] += amount; + + emit Transfer(msg.sender, sender, receiver, tokenId, amount); + + return true; + } +} diff --git a/src/misc/ERC6909NFT.sol b/src/misc/ERC6909NFT.sol new file mode 100644 index 000000000..00ff04074 --- /dev/null +++ b/src/misc/ERC6909NFT.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {ERC6909} from "src/misc/ERC6909.sol"; +import {StringLib} from "src/misc/libraries/StringLib.sol"; +import {Auth} from "src/misc/Auth.sol"; +import {IERC6909NFT, IERC6909URIExt} from "src/misc/interfaces/IERC6909.sol"; + +contract ERC6909NFT is ERC6909, Auth, IERC6909NFT { + using StringLib for string; + + uint8 constant MAX_SUPPLY = 1; + + uint256 public latestTokenId; + + /// @inheritdoc IERC6909URIExt + string public contractURI; + /// @inheritdoc IERC6909URIExt + mapping(uint256 tokenId => string URI) public tokenURI; + + constructor(address deployer) Auth(deployer) {} + + /// @inheritdoc IERC6909NFT + function setTokenURI(uint256 tokenId, string memory URI) public auth { + tokenURI[tokenId] = URI; + + emit TokenURISet(tokenId, URI); + } + + /// @inheritdoc IERC6909NFT + function mint(address owner, string memory tokenURI_) public auth returns (uint256 tokenId) { + require(!tokenURI_.isEmpty(), EmptyURI()); + + tokenId = ++latestTokenId; + + _mint(owner, tokenId, MAX_SUPPLY); + + setTokenURI(tokenId, tokenURI_); + } + + /// @inheritdoc IERC6909NFT + function burn(uint256 tokenId) external { + _burn(msg.sender, tokenId, 1); + } + + // @inheritdoc IERC6909NFT + function setContractURI(string calldata URI) external auth { + contractURI = URI; + + emit ContractURISet(address(this), URI); + } +} diff --git a/src/misc/LinearAccrual.sol b/src/misc/LinearAccrual.sol new file mode 100644 index 000000000..7ba2ff690 --- /dev/null +++ b/src/misc/LinearAccrual.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {ILinearAccrual} from "src/misc/interfaces/ILinearAccrual.sol"; +import {Compounding, CompoundingPeriod} from "src/misc/libraries/Compounding.sol"; +import {MathLib} from "src/misc/libraries/MathLib.sol"; +import {d18, D18, mulUint128} from "src/misc/types/D18.sol"; + +contract LinearAccrual is ILinearAccrual { + using MathLib for uint128; + using MathLib for uint256; + using MathLib for int128; + + mapping(bytes32 rateId => Rate rate) public rates; + mapping(bytes32 rateId => Group group) public groups; + + //---------------------------------------------------------------------------------------------- + // Rate updates + //---------------------------------------------------------------------------------------------- + + /// @inheritdoc ILinearAccrual + function drip(bytes32 rateId) public { + Rate storage rate = rates[rateId]; + + // Short circuit to save gas + if (rate.lastUpdated == uint64(block.timestamp)) { + return; + } + + Group memory group = groups[rateId]; + + // Determine number of full compounding periods passed since last update + uint64 periodsPassed = Compounding.getPeriodsPassed(group.period, rate.lastUpdated); + + if (periodsPassed > 0) { + rate.accumulatedRate = d18( + rate.accumulatedRate.mulUint128( + uint256(group.ratePerPeriod.raw()).rpow(periodsPassed, 1e18).toUint128(), MathLib.Rounding.Up + ) + ); + + emit RateAccumulated(rateId, rate.accumulatedRate.raw(), periodsPassed); + rate.lastUpdated = uint64(block.timestamp); + } + } + + //---------------------------------------------------------------------------------------------- + // Rate registration + //---------------------------------------------------------------------------------------------- + + /// @inheritdoc ILinearAccrual + function registerRateId(uint128 ratePerPeriod_, CompoundingPeriod period) public returns (bytes32 rateId) { + D18 ratePerPeriod = d18(ratePerPeriod_); + Group memory group = Group(ratePerPeriod, period); + + rateId = keccak256(abi.encode(group)); + + require(rates[rateId].lastUpdated == 0, RateIdExists(rateId, ratePerPeriod.raw(), period)); + + groups[rateId] = group; + rates[rateId] = Rate(ratePerPeriod, uint64(block.timestamp)); + + emit NewRateId(rateId, ratePerPeriod.raw(), period); + } + + //---------------------------------------------------------------------------------------------- + // View methods + //---------------------------------------------------------------------------------------------- + + /// @inheritdoc ILinearAccrual + function rateIdExists(bytes32 rateId) public view returns (bool) { + return rates[rateId].lastUpdated > 0; + } + + /// @inheritdoc ILinearAccrual + function getRateId(uint128 rate, CompoundingPeriod period) public pure returns (bytes32) { + Group memory group = Group(d18(rate), period); + + return keccak256(abi.encode(group)); + } + + /// @inheritdoc ILinearAccrual + function getModifiedNormalizedDebt(bytes32 rateId, int128 prevNormalizedDebt, int128 debtChange) + external + view + returns (int128 newNormalizedDebt) + { + _requireNonZeroUpdatedRateId(rateId); + + if (debtChange >= 0) { + return prevNormalizedDebt + + rates[rateId].accumulatedRate.reciprocalMulUint128(uint128(debtChange), MathLib.Rounding.Up).toInt128(); + } else { + return prevNormalizedDebt + - rates[rateId].accumulatedRate.reciprocalMulUint128(uint128(-debtChange), MathLib.Rounding.Up).toInt128(); + } + } + + /// @inheritdoc ILinearAccrual + function getRenormalizedDebt(bytes32 oldRateId, bytes32 newRateId, int128 prevNormalizedDebt) + external + view + returns (int128 newNormalizedDebt) + { + _requireNonZeroUpdatedRateId(newRateId); + + int128 debt_ = debt(oldRateId, prevNormalizedDebt); + + if (debt_ >= 0) { + return rates[newRateId].accumulatedRate.reciprocalMulUint128( + debt_.toUint256().toUint128(), MathLib.Rounding.Up + ).toInt128(); + } else { + return -( + rates[newRateId].accumulatedRate.reciprocalMulUint128( + (-debt_).toUint256().toUint128(), MathLib.Rounding.Up + ).toInt128() + ); + } + } + + /// @inheritdoc ILinearAccrual + function debt(bytes32 rateId, int128 normalizedDebt) public view returns (int128) { + _requireNonZeroUpdatedRateId(rateId); + + // Casting to int128 safe because we don't exceed number of digits of normalizedDebt + // Casting to uint256 necessary for mulDiv + if (normalizedDebt >= 0) { + return normalizedDebt.toUint256().mulDiv(rates[rateId].accumulatedRate.raw(), 1e18).toUint128().toInt128(); + } else { + return + -(-normalizedDebt).toUint256().mulDiv(rates[rateId].accumulatedRate.raw(), 1e18).toUint128().toInt128(); + } + } + + //---------------------------------------------------------------------------------------------- + // Internal methods + //---------------------------------------------------------------------------------------------- + + /// @notice Ensures the given rate id was updated in the current block and is not the zero-rate. + /// @dev Throws if rate has not been updated in the current block + /// @dev Throws if rate is zero-rate + /// @param rateId Identifier of the rate group + function _requireNonZeroUpdatedRateId(bytes32 rateId) internal view { + require(rates[rateId].lastUpdated != 0 && rates[rateId].accumulatedRate.raw() != 0, RateIdMissing(rateId)); + require(rates[rateId].lastUpdated == block.timestamp, RateIdOutdated(rateId, rates[rateId].lastUpdated)); + } +} diff --git a/src/misc/interfaces/ILinearAccrual.sol b/src/misc/interfaces/ILinearAccrual.sol new file mode 100644 index 000000000..669a26d11 --- /dev/null +++ b/src/misc/interfaces/ILinearAccrual.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +import {CompoundingPeriod} from "src/misc/libraries/Compounding.sol"; +import {D18} from "src/misc/types/D18.sol"; + +interface ILinearAccrual { + /// @dev Represents the rate accumulator and the timestamp of the last rate update + struct Rate { + /// @dev Accumulated rate index over time + /// @dev Assumes rate to be prefixed by 1, i.e. 5% rate shall be represented as 1.05 = d18(1e18 + 5e16) + D18 accumulatedRate; + /// @dev Timestamp of last rate update + uint64 lastUpdated; + } + + /// @dev Each group corresponds to a particular compound period and the accrual rate per period + struct Group { + /// @dev Rate per compound period + /// @dev Assumes rate to be prefixed by 1, i.e. 5% rate shall be represented as 1.05 = d18(1e18 + 5e16) + D18 ratePerPeriod; + /// @dev Duration of compound period + CompoundingPeriod period; + } + + /// Events + event NewRateId(bytes32 indexed rateId, uint128 indexed ratePerPeriod, CompoundingPeriod period); + event RateAccumulated(bytes32 indexed rateId, uint128 indexed rate, uint64 periodsPassed); + + /// Errors + error RateIdExists(bytes32 rateId, uint128 ratePerPeriod, CompoundingPeriod period); + error RateIdMissing(bytes32 rateId); + error RateIdOutdated(bytes32 rateId, uint64 lastUpdated); + + /// @notice Updates the accumulated rate of the corresponding identifier based on the periods which have passed + /// since the last update + /// @param rateId the id of the interest rate group + function drip(bytes32 rateId) external; + + /// @notice Registers the rate identifier for the given rate and compound period and returns it. + /// @dev Throws if rate has been updated once already implying it has been registered before + /// + /// @param ratePerPeriod Rate per compound period + /// @param period Compounding schedule + function registerRateId(uint128 ratePerPeriod, CompoundingPeriod period) external returns (bytes32 rateId); + + /// @notice Returns whether the rate identifier has been regsitered. + /// + /// @param rateId the id of the interest rate group + function rateIdExists(bytes32 rateId) external view returns (bool); + + /// @notice Returns the rate identifier for the given rate and compound period. + /// + /// @param ratePerPeriod Rate per compound period + /// @param period Compounding schedule + function getRateId(uint128 ratePerPeriod, CompoundingPeriod period) external pure returns (bytes32 rateId); + + /// @notice Returns the sum of the current normalized debt and the normalized change. + /// @dev Throws if rate has not been updated in the current block + /// @dev Throws if rate is zero-rate + /// + /// @param rateId Identifier of the rate group + /// @param prevNormalizedDebt Normalized debt before decreasing + /// @param debtChange The amount by which we modify the debt + function getModifiedNormalizedDebt(bytes32 rateId, int128 prevNormalizedDebt, int128 debtChange) + external + view + returns (int128 newNormalizedDebt); + + /// @notice Returns the renormalized debt based on the current rate group after transitioning normalization from + /// the previous one. + /// @dev Throws if rate has not been updated in the current block + /// @dev Throws if rate is zero-rate + /// + /// @param oldRateId Identifier of the previous rate group + /// @param newRateId Identifier of the current rate group + /// @param prevNormalizedDebt Normalized debt under previous rate group + function getRenormalizedDebt(bytes32 oldRateId, bytes32 newRateId, int128 prevNormalizedDebt) + external + view + returns (int128 newNormalizedDebt); + + /// @notice Returns the current debt without normalization based on actual block.timestamp (now) and the + /// accumulated rate. + /// @dev Throws if rate has not been updated in the current block + /// @dev Throws if rate is zero-rate + /// @param rateId Identifier of the rate group + /// @param normalizedDebt Normalized debt from which we derive the unnormalized debt + function debt(bytes32 rateId, int128 normalizedDebt) external view returns (int128 unnormalizedDebt); +} diff --git a/src/misc/libraries/Compounding.sol b/src/misc/libraries/Compounding.sol new file mode 100644 index 000000000..5e6ee1367 --- /dev/null +++ b/src/misc/libraries/Compounding.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +enum CompoundingPeriod { + Secondly, + Daily +} + +library Compounding { + uint64 constant SECONDS_PER_DAY = 86400; // 60 * 60 * 24 + + /// @notice Returns the amount of seconds for the given compounding period. + /// + /// @dev Default case is `CompoundingPeriod.Daily`. + function getSeconds(CompoundingPeriod period) public pure returns (uint64) { + if (period == CompoundingPeriod.Daily) return SECONDS_PER_DAY; + else return 1; + } + + /// @notice Returns the number of full compounding periods that have passed since a given timestamp. + /// + /// @dev Default case is `CompoundingPeriod.Daily` and returns 0 for any given future timestamp. + function getPeriodsPassed(CompoundingPeriod period, uint64 startTimestamp) public view returns (uint64) { + if (startTimestamp >= block.timestamp) { + return 0; + } else if (period == CompoundingPeriod.Daily) { + uint64 startDay = startTimestamp / SECONDS_PER_DAY; + uint64 nowDay = uint64(block.timestamp) / SECONDS_PER_DAY; + return nowDay - startDay; + } else { + return uint64(block.timestamp) - startTimestamp; + } + } +} diff --git a/src/misc/libraries/MathLib.sol b/src/misc/libraries/MathLib.sol index 4681b2f72..5a4b5adb5 100644 --- a/src/misc/libraries/MathLib.sol +++ b/src/misc/libraries/MathLib.sol @@ -170,12 +170,26 @@ library MathLib { return uint64(value); } + /// @notice Safe type conversion from uint128 to int128 + function toInt128(uint128 _value) internal pure returns (int128) { + require(_value <= uint128(type(int128).max), "MathLib/uint128-to-int128-overflow"); + + return int128(_value); + } /// @notice Safe type conversion from uint256 to uint128. + function toUint128(uint256 value) internal pure returns (uint128) { require(value <= type(uint128).max, Uint128_Overflow()); return uint128(value); } + /// @notice Safe type conversion from int128 to uint128 + function toUint256(int128 _value) internal pure returns (uint256) { + require(_value >= 0, "MathLib/int128-to-uint256-underflow"); + + return uint256(int256(_value)); + } + /// @notice Returns the smallest of two numbers. function min(uint256 a, uint256 b) internal pure returns (uint256) { return a > b ? b : a; diff --git a/src/misc/types/D18.sol b/src/misc/types/D18.sol index b510596a9..f2422ed37 100644 --- a/src/misc/types/D18.sol +++ b/src/misc/types/D18.sol @@ -91,6 +91,10 @@ function eq(D18 a, D18 b) pure returns (bool) { return D18.unwrap(a) == D18.unwrap(b); } +function lte(D18 a, D18 b) pure returns (bool) { + return D18.unwrap(a) <= D18.unwrap(b); +} + function isZero(D18 a) pure returns (bool) { return D18.unwrap(a) == 0; } @@ -107,6 +111,7 @@ using { add as +, sub as -, divD18 as /, + lte as <=, eq, mulD18 as *, mulUint128, diff --git a/src/spoke/libraries/UpdateContractMessageLib.sol b/src/spoke/libraries/UpdateContractMessageLib.sol index 47b853040..7a3fc73c2 100644 --- a/src/spoke/libraries/UpdateContractMessageLib.sol +++ b/src/spoke/libraries/UpdateContractMessageLib.sol @@ -10,7 +10,9 @@ enum UpdateContractType { Valuation, SyncDepositMaxReserve, UpdateAddress, - Policy + Policy, + LoanMaxBorrowAmount, + LoanRate } library UpdateContractMessageLib { @@ -115,4 +117,50 @@ library UpdateContractMessageLib { function serialize(UpdateContractPolicy memory t) internal pure returns (bytes memory) { return abi.encodePacked(UpdateContractType.Policy, t.who, t.what); } + + //--------------------------------------- + // UpdateContract.LoanMaxBorrowAmount (submsg) + //--------------------------------------- + + struct UpdateContractLoanMaxBorrowAmount { + uint128 assetId; + uint128 maxBorrowAmount; + } + + function deserializeUpdateContractLoanMaxBorrowAmount(bytes memory data) + internal + pure + returns (UpdateContractLoanMaxBorrowAmount memory) + { + require(updateContractType(data) == UpdateContractType.LoanMaxBorrowAmount, UnknownMessageType()); + + return UpdateContractLoanMaxBorrowAmount({assetId: data.toUint128(1), maxBorrowAmount: data.toUint128(17)}); + } + + function serialize(UpdateContractLoanMaxBorrowAmount memory t) internal pure returns (bytes memory) { + return abi.encodePacked(UpdateContractType.LoanMaxBorrowAmount, t.assetId, t.maxBorrowAmount); + } + + //--------------------------------------- + // UpdateContract.LoanRate (submsg) + //--------------------------------------- + + struct UpdateContractLoanRate { + uint128 assetId; + bytes32 rateId; + } + + function deserializeUpdateContractLoanRate(bytes memory data) + internal + pure + returns (UpdateContractLoanRate memory) + { + require(updateContractType(data) == UpdateContractType.LoanRate, UnknownMessageType()); + + return UpdateContractLoanRate({assetId: data.toUint128(1), rateId: data.toBytes32(17)}); + } + + function serialize(UpdateContractLoanRate memory t) internal pure returns (bytes memory) { + return abi.encodePacked(UpdateContractType.LoanRate, t.assetId, t.rateId); + } }