diff --git a/packages/contracts/.openzeppelin/goerli.json b/packages/contracts/.openzeppelin/goerli.json index 796519529..044f6e01f 100644 --- a/packages/contracts/.openzeppelin/goerli.json +++ b/packages/contracts/.openzeppelin/goerli.json @@ -163,11 +163,7 @@ }, "t_enum(CollateralType)25222": { "label": "enum CollateralType", - "members": [ - "ERC20", - "ERC721", - "ERC1155" - ], + "members": ["ERC20", "ERC721", "ERC1155"], "numberOfBytes": "1" }, "t_mapping(t_address,t_struct(Collateral)25232_storage)": { @@ -712,18 +708,12 @@ }, "t_enum(PaymentCycleType)27017": { "label": "enum PaymentCycleType", - "members": [ - "Seconds", - "Monthly" - ], + "members": ["Seconds", "Monthly"], "numberOfBytes": "1" }, "t_enum(PaymentType)27014": { "label": "enum PaymentType", - "members": [ - "EMI", - "Bullet" - ], + "members": ["EMI", "Bullet"], "numberOfBytes": "1" }, "t_mapping(t_address,t_array(t_uint256)dyn_storage)": { @@ -1144,10 +1134,7 @@ }, "t_enum(AllocationStrategy)19499": { "label": "enum IMarketLiquidityRewards.AllocationStrategy", - "members": [ - "BORROWER", - "LENDER" - ], + "members": ["BORROWER", "LENDER"], "numberOfBytes": "1" }, "t_mapping(t_uint256,t_bool)": { @@ -1598,18 +1585,12 @@ }, "t_enum(PaymentCycleType)27017": { "label": "enum PaymentCycleType", - "members": [ - "Seconds", - "Monthly" - ], + "members": ["Seconds", "Monthly"], "numberOfBytes": "1" }, "t_enum(PaymentType)27014": { "label": "enum PaymentType", - "members": [ - "EMI", - "Bullet" - ], + "members": ["EMI", "Bullet"], "numberOfBytes": "1" }, "t_mapping(t_address,t_bytes32)": { @@ -2132,18 +2113,12 @@ }, "t_enum(PaymentCycleType)27275": { "label": "enum PaymentCycleType", - "members": [ - "Seconds", - "Monthly" - ], + "members": ["Seconds", "Monthly"], "numberOfBytes": "1" }, "t_enum(PaymentType)27272": { "label": "enum PaymentType", - "members": [ - "EMI", - "Bullet" - ], + "members": ["EMI", "Bullet"], "numberOfBytes": "1" }, "t_mapping(t_address,t_bytes32)": { @@ -2627,18 +2602,12 @@ }, "t_enum(PaymentCycleType)22798": { "label": "enum PaymentCycleType", - "members": [ - "Seconds", - "Monthly" - ], + "members": ["Seconds", "Monthly"], "numberOfBytes": "1" }, "t_enum(PaymentType)22795": { "label": "enum PaymentType", - "members": [ - "EMI", - "Bullet" - ], + "members": ["EMI", "Bullet"], "numberOfBytes": "1" }, "t_mapping(t_address,t_array(t_uint256)dyn_storage)": { @@ -3011,11 +2980,7 @@ }, "t_enum(CollateralType)25478": { "label": "enum CollateralType", - "members": [ - "ERC20", - "ERC721", - "ERC1155" - ], + "members": ["ERC20", "ERC721", "ERC1155"], "numberOfBytes": "1" }, "t_mapping(t_address,t_struct(Collateral)25488_storage)": { @@ -3205,11 +3170,7 @@ }, "t_enum(CollateralType)25478": { "label": "enum CollateralType", - "members": [ - "ERC20", - "ERC721", - "ERC1155" - ], + "members": ["ERC20", "ERC721", "ERC1155"], "numberOfBytes": "1" }, "t_mapping(t_address,t_struct(Collateral)25488_storage)": { @@ -3340,11 +3301,7 @@ }, "t_enum(CollateralType)1731": { "label": "enum CollateralType", - "members": [ - "ERC20", - "ERC721", - "ERC1155" - ], + "members": ["ERC20", "ERC721", "ERC1155"], "numberOfBytes": "1" }, "t_mapping(t_address,t_struct(Collateral)1741_storage)": { @@ -4041,6 +3998,170 @@ } } } + }, + "f7e687d03348b5404e02b20629f7fc8f48e86b0f51f9b7aa9b23ddbaa83efc01": { + "address": "0x62babFC668494145051a473112De8D3e93d3927E", + "txHash": "0x1de99cc00cf5eae3fb3b169ab9c6eb29a19dc827b951e1b3e7103509572f04f9", + "layout": { + "solcVersion": "0.8.9", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_owner", + "offset": 0, + "slot": "51", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41" + }, + { + "label": "_name", + "offset": 0, + "slot": "151", + "type": "t_string_storage", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:25" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "152", + "type": "t_string_storage", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:28" + }, + { + "label": "_owners", + "offset": 0, + "slot": "153", + "type": "t_mapping(t_uint256,t_address)", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:31" + }, + { + "label": "_balances", + "offset": 0, + "slot": "154", + "type": "t_mapping(t_address,t_uint256)", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:34" + }, + { + "label": "_tokenApprovals", + "offset": 0, + "slot": "155", + "type": "t_mapping(t_uint256,t_address)", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:37" + }, + { + "label": "_operatorApprovals", + "offset": 0, + "slot": "156", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:40" + }, + { + "label": "__gap", + "offset": 0, + "slot": "157", + "type": "t_array(t_uint256)44_storage", + "contract": "ERC721Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:514" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)44_storage": { + "label": "uint256[44]", + "numberOfBytes": "1408" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_address,t_bool))": { + "label": "mapping(address => mapping(address => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_address)": { + "label": "mapping(uint256 => address)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } } } } diff --git a/packages/contracts/contracts/LenderManager.sol b/packages/contracts/contracts/LenderManager.sol index f9e52b1d9..d19977872 100644 --- a/packages/contracts/contracts/LenderManager.sol +++ b/packages/contracts/contracts/LenderManager.sol @@ -6,11 +6,19 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import "@openzeppelin/contracts/utils/Base64.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + // Interfaces import "./interfaces/ILenderManager.sol"; import "./interfaces/ITellerV2.sol"; +import "./interfaces/ITellerV2Storage.sol"; +import "./interfaces/ICollateralManager.sol"; import "./interfaces/IMarketRegistry.sol"; +import { LenderManagerArt } from "./libraries/LenderManagerArt.sol"; +import { CollateralType, Collateral } from "./interfaces/escrow/ICollateralEscrowV1.sol"; + contract LenderManager is Initializable, OwnableUpgradeable, @@ -78,7 +86,92 @@ contract LenderManager is require(_hasMarketVerification(to, tokenId), "Not approved by market"); } - function _baseURI() internal view override returns (string memory) { - return ""; + struct LoanInformation { + address principalTokenAddress; + uint256 principalAmount; + uint16 interestRate; + uint32 loanDuration; + } + + function _getLoanInformation(uint256 tokenId) + internal + view + returns (LoanInformation memory loanInformation_) + { + Bid memory bid = ITellerV2Storage(owner()).bids(tokenId); + + loanInformation_ = LoanInformation({ + principalTokenAddress: address(bid.loanDetails.lendingToken), + principalAmount: bid.loanDetails.principal, + interestRate: bid.terms.APR, + loanDuration: bid.loanDetails.loanDuration + }); + } + + function _getCollateralInformation(uint256 tokenId) + internal + view + returns (Collateral memory collateral_) + { + address collateralManager = ITellerV2Storage(owner()) + .collateralManager(); + + Collateral[] memory collateralArray = ICollateralManager( + collateralManager + ).getCollateralInfo(tokenId); + + if (collateralArray.length > 0) { + collateral_ = collateralArray[0]; + } + } + + function tokenURI(uint256 tokenId) + public + view + override + returns (string memory) + { + LoanInformation memory loanInformation = _getLoanInformation(tokenId); + + Collateral memory collateral = _getCollateralInformation(tokenId); + + string memory image_svg_encoded = Base64.encode( + bytes( + LenderManagerArt.generateSVG( + tokenId, //tokenId == bidId + loanInformation.principalAmount, + loanInformation.principalTokenAddress, + collateral, + loanInformation.interestRate, + loanInformation.loanDuration + ) + ) + ); + + string memory name = "Teller Loan NFT"; + string + memory description = "This token represents ownership of a loan. Repayments of principal and interest will be sent to the owner of this token. If the loan defaults, the owner of this token will be able to claim the underlying collateral. Please externally verify the parameter of the loan as this rendering is only a summary."; + + string memory encoded_svg = string( + abi.encodePacked( + "data:application/json;base64,", + Base64.encode( + bytes( + abi.encodePacked( + '{"name":"', + name, + '", "description":"', + description, + '", "image": "', + "data:image/svg+xml;base64,", + image_svg_encoded, + '"}' + ) + ) + ) + ) + ); + + return encoded_svg; } } diff --git a/packages/contracts/contracts/interfaces/ITellerV2Storage.sol b/packages/contracts/contracts/interfaces/ITellerV2Storage.sol new file mode 100644 index 000000000..ad7eb8812 --- /dev/null +++ b/packages/contracts/contracts/interfaces/ITellerV2Storage.sol @@ -0,0 +1,7 @@ +import { Bid } from "../TellerV2Storage.sol"; + +interface ITellerV2Storage { + function bids(uint256 _bidId) external view returns (Bid memory); + + function collateralManager() external view returns (address); +} diff --git a/packages/contracts/contracts/libraries/LenderManagerArt.sol b/packages/contracts/contracts/libraries/LenderManagerArt.sol new file mode 100644 index 000000000..23655e8e7 --- /dev/null +++ b/packages/contracts/contracts/libraries/LenderManagerArt.sol @@ -0,0 +1,464 @@ +/** + *Submitted for verification at Etherscan.io on 2023-06-21 + */ + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/utils/Address.sol"; + +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts/utils/math/SignedMath.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + +import { Collateral, CollateralType } from "../interfaces/escrow/ICollateralEscrowV1.sol"; + +//must use this custom interface since the OZ one doesnt support decimals +interface IERC20 { + function totalSupply() external view returns (uint256); + + function balanceOf(address account) external view returns (uint256); + + function transfer(address recipient, uint256 amount) + external + returns (bool); + + function allowance(address owner, address spender) + external + view + returns (uint256); + + function approve(address spender, uint256 amount) external returns (bool); + + function transferFrom(address sender, address recipient, uint256 amount) + external + returns (bool); + + function decimals() external view returns (uint8); + + function symbol() external view returns (string memory); +} + +library LenderManagerArt { + using Strings for uint256; + using Strings for uint32; + using Strings for uint16; + using Address for address; + + bytes constant _bg_defs_filter = + abi.encodePacked( + "", + "", + "", + "", + "", + "", + "", + "" + "", + "" + ); + + bytes constant _bg_defs_clip_path = + abi.encodePacked( + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ); + + bytes constant _bg_defs_mask = + abi.encodePacked( + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ); + + bytes constant _bg_defs = + abi.encodePacked( + "", + _bg_defs_filter, + _bg_defs_clip_path, + _bg_defs_mask, + "" + ); + + bytes constant _clip_path_corners = + abi.encodePacked( + "", + "", + "", + "", + "", + "", + "", + "", + "" + ); + + bytes constant _teller_logo_path_1 = + abi.encodePacked( + "", + "", + "", + "", + "", + "", + "", + "" + ); + + bytes constant _teller_logo_path_2 = + abi.encodePacked( + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ); + + bytes constant _teller_logo = + abi.encodePacked( + "", + "", + "", + _teller_logo_path_1, + _teller_logo_path_2, + "" + ); + + function _generate_large_title(string memory amount, string memory symbol) + public + pure + returns (string memory) + { + return + string( + abi.encodePacked( + "", + "", + "AMOUNT", + "", + amount, + " ", + symbol, + "", + "", + "" + ) + ); + } + + function _generate_text_label( + string memory label, + string memory value, + uint256 y_offset + ) public pure returns (string memory) { + return + string( + abi.encodePacked( + "", + "", + "", + "", + label, + "", + value, + "", + "" + ) + ); + } + + function _get_token_amount_formatted(uint256 amount, uint256 decimals) + public + pure + returns (string memory) + { + uint256 precision = Math.min(3, decimals); + + uint256 before_decimal = amount / (10**decimals); + + uint256 after_decimal = amount % (10**decimals); + + // truncate to the required precision + after_decimal = after_decimal / (10**(decimals - precision)); + + if (before_decimal >= 1000000000000000) { + return "> RANGE"; + } + + if (before_decimal >= 1000000000000) { + uint256 trillions = before_decimal / 1000000000000; + uint256 billions = (before_decimal % 1000000000000) / 100000000000; // Get the first digit after the decimal point + return + string( + abi.encodePacked( + Strings.toString(trillions), + ".", + Strings.toString(billions), + "T" + ) + ); + } + + if (before_decimal >= 1000000000) { + uint256 billions = before_decimal / 1000000000; + uint256 millions = (before_decimal % 1000000000) / 100000000; // Get the first digit after the decimal point + return + string( + abi.encodePacked( + Strings.toString(billions), + ".", + Strings.toString(millions), + "B" + ) + ); + } + + if (before_decimal >= 1000000) { + uint256 millions = before_decimal / 1000000; + uint256 thousands = (before_decimal % 1000000) / 100000; // Get the first digit after the decimal point + return + string( + abi.encodePacked( + Strings.toString(millions), + ".", + Strings.toString(thousands), + "M" + ) + ); + } + + if (before_decimal >= 1000) { + uint256 fullThousands = before_decimal / 1000; + uint256 remainder = (before_decimal % 1000) / 100; // Get the first digit after the decimal point + return + string( + abi.encodePacked( + Strings.toString(fullThousands), + ".", + Strings.toString(remainder), + "K" + ) + ); + } + + return + string( + abi.encodePacked( + Strings.toString(before_decimal), + ".", + Strings.toString(after_decimal) + ) + ); + } + + function _buildSvgData( + string memory loanId, + string memory principalAmountFormatted, + string memory principalTokenSymbol, + string memory collateralLabel, + string memory interestRateLabel, + string memory loanDurationLabel + ) internal pure returns (string memory) { + return + string( + abi.encodePacked( + "", + _bg_defs, + _clip_path_corners, + _generate_large_title( + principalAmountFormatted, + principalTokenSymbol + ), + _teller_logo, + _generate_text_label("Loan ID: ", loanId, 354), + _generate_text_label("Collateral: ", collateralLabel, 384), + _generate_text_label("APR: ", interestRateLabel, 414), + _generate_text_label("Duration: ", loanDurationLabel, 444), + "" + ) + ); + } + + function _get_token_decimals(address token) public view returns (uint256) { + bytes memory data = abi.encodeWithSignature("decimals()"); + + if (token.code.length == 0) { + return 0; + } + + (bool success, bytes memory result) = token.staticcall(data); + + if (!success) { + return 0; + } + + // Decode the result + uint8 decimals = abi.decode(result, (uint8)); + + return decimals; + } + + function _get_interest_rate_formatted(uint16 interestRate) + public + pure + returns (string memory) + { + return string(abi.encodePacked((interestRate / 100).toString(), " %")); + } + + function _get_duration_formatted(uint32 sec) + public + pure + returns (string memory) + { + uint32 _months = sec / 4 weeks; + uint32 _weeks = sec / 1 weeks; + uint32 _days = sec / 1 days; + uint32 _hours = sec / 1 hours; + uint32 _minutes = sec / 1 minutes; + + if (_months >= 2) { + return string(abi.encodePacked(_months.toString(), " months")); + } else if (_weeks >= 2) { + return string(abi.encodePacked(_weeks.toString(), " weeks")); + } else if (_days >= 2) { + return string(abi.encodePacked(_days.toString(), " days")); + } else if (_hours >= 2) { + return string(abi.encodePacked(_hours.toString(), " hours")); + } else { + return string(abi.encodePacked(_minutes.toString(), " minutes")); + } + } + + function _get_token_symbol(address nftContract, string memory _fallback) + public + view + returns (string memory) + { + bytes memory data = abi.encodeWithSignature("symbol()"); + + if (nftContract.code.length == 0) { + return _fallback; + } + + (bool success, bytes memory result) = nftContract.staticcall(data); + + if (!success) { + return _fallback; + } + + // Decode the result from bytes to string + string memory symbol = abi.decode(result, (string)); + + return symbol; + } + + function _get_collateral_label(Collateral memory collateral) + public + view + returns (string memory) + { + if (collateral._collateralAddress == address(0)) { + return "None"; + } + + if (collateral._collateralType == CollateralType.ERC20) { + string + memory collateralAmountFormatted = _get_token_amount_formatted( + collateral._amount, + _get_token_decimals(collateral._collateralAddress) + ); + + string memory collateralTokenSymbol = _get_token_symbol( + collateral._collateralAddress, + "?" + ); + + return + string( + abi.encodePacked( + collateralAmountFormatted, + " ", + collateralTokenSymbol + ) + ); + } + + if (collateral._collateralType == CollateralType.ERC721) { + return _get_token_symbol(collateral._collateralAddress, "ERC721"); + } + + if (collateral._collateralType == CollateralType.ERC1155) { + return _get_token_symbol(collateral._collateralAddress, "ERC1155"); + } + } + + function generateSVG( + uint256 bidId, + uint256 principalAmount, + address principalTokenAddress, + Collateral memory collateral, + uint16 interestRate, + uint32 loanDuration + ) public view returns (string memory) { + string memory principalAmountFormatted = _get_token_amount_formatted( + principalAmount, + _get_token_decimals(principalTokenAddress) + ); + + string memory principalTokenSymbol = _get_token_symbol( + principalTokenAddress, + "?" + ); + + string memory svgData = _buildSvgData( + (bidId).toString(), + principalAmountFormatted, + principalTokenSymbol, + _get_collateral_label(collateral), + _get_interest_rate_formatted(interestRate), + _get_duration_formatted(loanDuration) + ); + + return svgData; + } +} diff --git a/packages/contracts/deploy/lender_manager/deploy.ts b/packages/contracts/deploy/lender_manager/deploy.ts index 247f3bcca..414bd2dd1 100644 --- a/packages/contracts/deploy/lender_manager/deploy.ts +++ b/packages/contracts/deploy/lender_manager/deploy.ts @@ -3,9 +3,14 @@ import { DeployFunction } from 'hardhat-deploy/dist/types' const deployFn: DeployFunction = async (hre) => { const marketRegistry = await hre.contracts.get('MarketRegistry') + const lenderManagerArt = await hre.contracts.get('LenderManagerArt') + const lenderManager = await hre.deployProxy('LenderManager', { constructorArgs: [await marketRegistry.getAddress()], unsafeAllow: ['constructor', 'state-variable-immutable'], + libraries: { + LenderManagerArt: lenderManagerArt.address, + }, }) return true @@ -14,5 +19,8 @@ const deployFn: DeployFunction = async (hre) => { // tags and deployment deployFn.id = 'lender-manager:deploy' deployFn.tags = ['lender-manager', 'lender-manager:deploy'] -deployFn.dependencies = ['market-registry:deploy'] +deployFn.dependencies = [ + 'market-registry:deploy', + 'lender-manager:lender-manager-art', +] export default deployFn diff --git a/packages/contracts/deploy/lender_manager/lender_manager_art.ts b/packages/contracts/deploy/lender_manager/lender_manager_art.ts new file mode 100644 index 000000000..68de96202 --- /dev/null +++ b/packages/contracts/deploy/lender_manager/lender_manager_art.ts @@ -0,0 +1,15 @@ +import { DeployFunction } from 'hardhat-deploy/dist/types' +import { deploy } from 'helpers/deploy-helpers' + +const deployFn: DeployFunction = async (hre) => { + const lenderManagerArt = await deploy({ + contract: 'LenderManagerArt', + hre, + }) +} + +// tags and deployment +deployFn.id = 'lender-manager:lender-manager-art' +deployFn.tags = ['lender-manager', 'lender-manager:lender-manager-art'] +deployFn.dependencies = [''] +export default deployFn diff --git a/packages/contracts/deploy/upgrades/01_lender_commitment_merkle.ts b/packages/contracts/deploy/upgrades/01_merkle_root_lender_art.ts similarity index 63% rename from packages/contracts/deploy/upgrades/01_lender_commitment_merkle.ts rename to packages/contracts/deploy/upgrades/01_merkle_root_lender_art.ts index 8231e716f..c1954b09f 100644 --- a/packages/contracts/deploy/upgrades/01_lender_commitment_merkle.ts +++ b/packages/contracts/deploy/upgrades/01_merkle_root_lender_art.ts @@ -3,7 +3,7 @@ import { DeployFunction } from 'hardhat-deploy/dist/types' const deployFn: DeployFunction = async (hre) => { hre.log('----------') hre.log('') - hre.log('LenderCommitmentForwarder: Proposing upgrade...') + hre.log('Proposing upgrade...') const tellerV2 = await hre.contracts.get('TellerV2') const marketRegistry = await hre.contracts.get('MarketRegistry') @@ -11,8 +11,11 @@ const deployFn: DeployFunction = async (hre) => { 'LenderCommitmentForwarder' ) + const lenderManager = await hre.contracts.get('LenderManager') + const lenderManagerArt = await hre.contracts.get('LenderManagerArt') + await hre.defender.proposeBatchTimelock( - 'Lender Commitment Forwarder Merkle Upgrade', + 'Merkle Root + Lender Art Upgrade', ` # LenderCommitmentForwarder @@ -20,6 +23,11 @@ const deployFn: DeployFunction = async (hre) => { * Adds two new collateral types, ERC721_MERKLE_PROOF and ERC1155_MERKLE_PROOF. * Add a new function acceptCommitmentWithProof which is explicitly used with these new types. * Merkle proofs can be used to create commitments for a set of tokenIds for an ERC721 or ERC1155 collection. + +# Lender Manager + +* Updates the tokenURI function so it returns an svg image rendering with loan summary data. + `, [ { @@ -36,6 +44,22 @@ const deployFn: DeployFunction = async (hre) => { ], }, }, + { + proxy: lenderManager.address, + implFactory: await hre.ethers.getContractFactory('LenderManager', { + libraries: { + LenderManagerArt: lenderManagerArt.address, + }, + }), + opts: { + unsafeAllow: [ + 'constructor', + 'state-variable-immutable', + 'external-library-linking', + ], + constructorArgs: [marketRegistry.address], + }, + }, ] ) @@ -47,17 +71,20 @@ const deployFn: DeployFunction = async (hre) => { } // tags and deployment -deployFn.id = 'lender-commitment-forwarder:merkle-upgrade' +deployFn.id = 'merkle-root-lender-art:upgrade' deployFn.tags = [ 'proposal', 'upgrade', 'lender-commitment-forwarder', - 'lender-commitment-forwarder:merkle-upgrade', + 'lender-manager', + 'merkle-root-lender-art:upgrade', ] deployFn.dependencies = [ 'market-registry:deploy', 'teller-v2:deploy', 'lender-commitment-forwarder:deploy', + 'market-registry:deploy', + 'lender-manager:deploy', ] deployFn.skip = async (hre) => { return ( diff --git a/packages/contracts/deployments/goerli/.migrations.json b/packages/contracts/deployments/goerli/.migrations.json index e1787d91a..8a6cca6a8 100644 --- a/packages/contracts/deployments/goerli/.migrations.json +++ b/packages/contracts/deployments/goerli/.migrations.json @@ -13,6 +13,6 @@ "escrow-vault:deploy": 1688578801, "default-proxy-admin:transfer": 1688587184, "sherlock-audit:upgrade": 1688587576, - "lender-commitment-forwarder:merkle-upgrade": 1688596796, + "merkle-root-lender-art:upgrade": 1688596796, "teller-v2:transfer-ownership-to-safe": 1688588967 } diff --git a/packages/contracts/teller-math-lib b/packages/contracts/teller-math-lib index 1dd27b8a1..c2a17d57a 160000 --- a/packages/contracts/teller-math-lib +++ b/packages/contracts/teller-math-lib @@ -1 +1 @@ -Subproject commit 1dd27b8a176b5fddd917ad1f752421b4b54a5204 +Subproject commit c2a17d57a11399ffdf2a9ed650275574ddd35fa4 diff --git a/packages/contracts/tests/LenderManagerArt_Override.sol b/packages/contracts/tests/LenderManagerArt_Override.sol new file mode 100644 index 000000000..2a45a3291 --- /dev/null +++ b/packages/contracts/tests/LenderManagerArt_Override.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { CollateralType, Collateral } from "../contracts/interfaces/escrow/ICollateralEscrowV1.sol"; + +import "lib/forge-std/src/console.sol"; + +import { Testable } from "./Testable.sol"; +import { LenderManagerArt } from "../contracts/libraries/LenderManagerArt.sol"; + +contract LenderManagerArt_Override {} diff --git a/packages/contracts/tests/LenderManagerArt_Test.sol b/packages/contracts/tests/LenderManagerArt_Test.sol new file mode 100644 index 000000000..579fc5946 --- /dev/null +++ b/packages/contracts/tests/LenderManagerArt_Test.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { CollateralType, Collateral } from "../contracts/interfaces/escrow/ICollateralEscrowV1.sol"; + +import "lib/forge-std/src/console.sol"; + +import { Testable } from "./Testable.sol"; +import { LenderManagerArt } from "../contracts/libraries/LenderManagerArt.sol"; + +import "./tokens/TestERC1155Token.sol"; +import "./tokens/TestERC721Token.sol"; +import "../contracts/mock/WethMock.sol"; + +contract LenderManagerArt_Test is Testable { + WethMock wethMock; + TestERC721Token erc721Mock; + TestERC1155Token erc1155Mock; + + constructor() {} + + function setUp() public { + wethMock = new WethMock(); + erc721Mock = new TestERC721Token("BAYC", "BAYC"); + erc1155Mock = new TestERC1155Token("SAND"); + } + + function test_generateSVG() public { + Collateral memory _collateral = Collateral({ + _collateralType: CollateralType.ERC721, + _collateralAddress: address(erc721Mock), + _amount: 1, + _tokenId: 150 + }); + + string memory svg = LenderManagerArt.generateSVG( + 22, + 82330000000000420055000, + address(wethMock), + _collateral, + 300, + 550000 + ); + + console.log("the svg:"); + console.log(svg); + } + + //add more unit tests here + function test_get_token_decimals() public { + uint256 decimals = LenderManagerArt._get_token_decimals( + address(wethMock) + ); + + assertEq(decimals, 18); + } + + function test_get_token_decimals_erc721() public { + uint256 decimals = LenderManagerArt._get_token_decimals( + address(erc721Mock) + ); + + assertEq(decimals, 0); + } + + function test_get_interest_rate_formatted() public { + string memory interestRate = LenderManagerArt + ._get_interest_rate_formatted(30000); + + assertEq(interestRate, "300 %"); + } + + function test_get_duration_formatted() public { + string memory duration = LenderManagerArt._get_duration_formatted( + 30000 + ); + + assertEq(duration, "8 hours"); + } + + function test_get_duration_formatted_2() public { + string memory duration = LenderManagerArt._get_duration_formatted( + 3000000 + ); + + assertEq(duration, "4 weeks"); + } + + function test_get_token_symbol() public { + string memory symbol = LenderManagerArt._get_token_symbol( + address(wethMock), + "fallback" + ); + + assertEq(symbol, "WETH"); + } + + function test_get_token_symbol_fallback() public { + string memory symbol = LenderManagerArt._get_token_symbol( + address(0), + "fallback" + ); + + assertEq(symbol, "fallback"); + } + + function test_get_collateral_label() public { + Collateral memory _collateral = Collateral({ + _collateralType: CollateralType.ERC721, + _collateralAddress: address(erc721Mock), + _amount: 1, + _tokenId: 150 + }); + + string memory label = LenderManagerArt._get_collateral_label( + _collateral + ); + + assertEq(label, "BAYC"); + } +} diff --git a/packages/contracts/tests/LenderManager_Test.sol b/packages/contracts/tests/LenderManager_Test.sol index 27713a3e0..cd759bb51 100644 --- a/packages/contracts/tests/LenderManager_Test.sol +++ b/packages/contracts/tests/LenderManager_Test.sol @@ -88,7 +88,11 @@ contract LenderManager_Test is Testable { string memory baseURI = lenderManager._baseURISuper(); - assertEq(baseURI, "", "Base URI is not correct"); + assertEq( + baseURI, + "", + "Base URI is not correct" + ); } function test_mint() public { diff --git a/packages/services/graph-node b/packages/services/graph-node deleted file mode 160000 index 3e733e3dd..000000000 --- a/packages/services/graph-node +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3e733e3dd69e6317ac0a6a49942f08330788cc6e