Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/hub/Accounting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down
186 changes: 186 additions & 0 deletions src/managers/LoansManager.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
107 changes: 107 additions & 0 deletions src/misc/ERC6909.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
52 changes: 52 additions & 0 deletions src/misc/ERC6909NFT.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading