diff --git a/contracts/mocks/tokens/Pandora.sol b/contracts/mocks/tokens/Pandora.sol new file mode 100644 index 000000000..2a05fd3bb --- /dev/null +++ b/contracts/mocks/tokens/Pandora.sol @@ -0,0 +1,480 @@ +//SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {Strings} from "../../dependencies/openzeppelin/contracts/Strings.sol"; + +abstract contract Ownable { + event OwnershipTransferred(address indexed user, address indexed newOwner); + + error Unauthorized(); + error InvalidOwner(); + + address public owner; + + modifier onlyOwner() virtual { + if (msg.sender != owner) revert Unauthorized(); + + _; + } + + constructor(address _owner) { + if (_owner == address(0)) revert InvalidOwner(); + + owner = _owner; + + emit OwnershipTransferred(address(0), _owner); + } + + function transferOwnership(address _owner) public virtual onlyOwner { + if (_owner == address(0)) revert InvalidOwner(); + + owner = _owner; + + emit OwnershipTransferred(msg.sender, _owner); + } + + function revokeOwnership() public virtual onlyOwner { + owner = address(0); + + emit OwnershipTransferred(msg.sender, address(0)); + } +} + +abstract contract ERC721Receiver { + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) external virtual returns (bytes4) { + return ERC721Receiver.onERC721Received.selector; + } +} + +/// @notice ERC404 +/// A gas-efficient, mixed ERC20 / ERC721 implementation +/// with native liquidity and fractionalization. +/// +/// This is an experimental standard designed to integrate +/// with pre-existing ERC20 / ERC721 support as smoothly as +/// possible. +/// +/// @dev In order to support full functionality of ERC20 and ERC721 +/// supply assumptions are made that slightly constraint usage. +/// Ensure decimals are sufficiently large (standard 18 recommended) +/// as ids are effectively encoded in the lowest range of amounts. +/// +/// NFTs are spent on ERC20 functions in a FILO queue, this is by +/// design. +/// +abstract contract ERC404 is Ownable { + // Events + event ERC20Transfer( + address indexed from, + address indexed to, + uint256 amount + ); + event Approval( + address indexed owner, + address indexed spender, + uint256 amount + ); + event Transfer( + address indexed from, + address indexed to, + uint256 indexed id + ); + event ERC721Approval( + address indexed owner, + address indexed spender, + uint256 indexed id + ); + event ApprovalForAll( + address indexed owner, + address indexed operator, + bool approved + ); + + // Errors + error NotFound(); + error AlreadyExists(); + error InvalidRecipient(); + error InvalidSender(); + error UnsafeRecipient(); + + // Metadata + /// @dev Token name + string public name; + + /// @dev Token symbol + string public symbol; + + /// @dev Decimals for fractional representation + uint8 public immutable decimals; + + /// @dev Total supply in fractionalized representation + uint256 public immutable totalSupply; + + /// @dev Current mint counter, monotonically increasing to ensure accurate ownership + uint256 public minted; + + // Mappings + /// @dev Balance of user in fractional representation + mapping(address => uint256) public balanceOf; + + /// @dev Allowance of user in fractional representation + mapping(address => mapping(address => uint256)) public allowance; + + /// @dev Approval in native representaion + mapping(uint256 => address) public getApproved; + + /// @dev Approval for all in native representation + mapping(address => mapping(address => bool)) public isApprovedForAll; + + /// @dev Owner of id in native representation + mapping(uint256 => address) internal _ownerOf; + + /// @dev Array of owned ids in native representation + mapping(address => uint256[]) internal _owned; + + /// @dev Tracks indices for the _owned mapping + mapping(uint256 => uint256) internal _ownedIndex; + + /// @dev Addresses whitelisted from minting / burning for gas savings (pairs, routers, etc) + mapping(address => bool) public whitelist; + + // Constructor + constructor( + string memory _name, + string memory _symbol, + uint8 _decimals, + uint256 _totalNativeSupply, + address _owner + ) Ownable(_owner) { + name = _name; + symbol = _symbol; + decimals = _decimals; + totalSupply = _totalNativeSupply * (10 ** decimals); + } + + /// @notice Initialization function to set pairs / etc + /// saving gas by avoiding mint / burn on unnecessary targets + function setWhitelist(address target, bool state) public onlyOwner { + whitelist[target] = state; + } + + /// @notice Function to find owner of a given native token + function ownerOf(uint256 id) public view virtual returns (address owner) { + owner = _ownerOf[id]; + + if (owner == address(0)) { + revert NotFound(); + } + } + + /// @notice tokenURI must be implemented by child contract + function tokenURI(uint256 id) public view virtual returns (string memory); + + /// @notice Function for token approvals + /// @dev This function assumes id / native if amount less than or equal to current max id + function approve( + address spender, + uint256 amountOrId + ) public virtual returns (bool) { + if (amountOrId <= minted && amountOrId > 0) { + address owner = _ownerOf[amountOrId]; + + if (msg.sender != owner && !isApprovedForAll[owner][msg.sender]) { + revert Unauthorized(); + } + + getApproved[amountOrId] = spender; + + emit Approval(owner, spender, amountOrId); + } else { + allowance[msg.sender][spender] = amountOrId; + + emit Approval(msg.sender, spender, amountOrId); + } + + return true; + } + + /// @notice Function native approvals + function setApprovalForAll(address operator, bool approved) public virtual { + isApprovedForAll[msg.sender][operator] = approved; + + emit ApprovalForAll(msg.sender, operator, approved); + } + + /// @notice Function for mixed transfers + /// @dev This function assumes id / native if amount less than or equal to current max id + function transferFrom( + address from, + address to, + uint256 amountOrId + ) public virtual { + if (amountOrId <= minted) { + if (from != _ownerOf[amountOrId]) { + revert InvalidSender(); + } + + if (to == address(0)) { + revert InvalidRecipient(); + } + + if ( + msg.sender != from && + !isApprovedForAll[from][msg.sender] && + msg.sender != getApproved[amountOrId] + ) { + revert Unauthorized(); + } + + balanceOf[from] -= _getUnit(); + + unchecked { + balanceOf[to] += _getUnit(); + } + + _ownerOf[amountOrId] = to; + delete getApproved[amountOrId]; + + // update _owned for sender + uint256 updatedId = _owned[from][_owned[from].length - 1]; + _owned[from][_ownedIndex[amountOrId]] = updatedId; + // pop + _owned[from].pop(); + // update index for the moved id + _ownedIndex[updatedId] = _ownedIndex[amountOrId]; + // push token to to owned + _owned[to].push(amountOrId); + // update index for to owned + _ownedIndex[amountOrId] = _owned[to].length - 1; + + emit Transfer(from, to, amountOrId); + emit ERC20Transfer(from, to, _getUnit()); + } else { + uint256 allowed = allowance[from][msg.sender]; + + if (allowed != type(uint256).max) + allowance[from][msg.sender] = allowed - amountOrId; + + _transfer(from, to, amountOrId); + } + } + + /// @notice Function for fractional transfers + function transfer( + address to, + uint256 amount + ) public virtual returns (bool) { + return _transfer(msg.sender, to, amount); + } + + /// @notice Function for native transfers with contract support + function safeTransferFrom( + address from, + address to, + uint256 id + ) public virtual { + transferFrom(from, to, id); + + if ( + to.code.length != 0 && + ERC721Receiver(to).onERC721Received(msg.sender, from, id, "") != + ERC721Receiver.onERC721Received.selector + ) { + revert UnsafeRecipient(); + } + } + + /// @notice Function for native transfers with contract support and callback data + function safeTransferFrom( + address from, + address to, + uint256 id, + bytes calldata data + ) public virtual { + transferFrom(from, to, id); + + if ( + to.code.length != 0 && + ERC721Receiver(to).onERC721Received(msg.sender, from, id, data) != + ERC721Receiver.onERC721Received.selector + ) { + revert UnsafeRecipient(); + } + } + + /// @notice Internal function for fractional transfers + function _transfer( + address from, + address to, + uint256 amount + ) internal returns (bool) { + uint256 unit = _getUnit(); + uint256 balanceBeforeSender = balanceOf[from]; + uint256 balanceBeforeReceiver = balanceOf[to]; + + balanceOf[from] -= amount; + + unchecked { + balanceOf[to] += amount; + } + + // Skip burn for certain addresses to save gas + if (!whitelist[from]) { + uint256 tokens_to_burn = (balanceBeforeSender / unit) - + (balanceOf[from] / unit); + for (uint256 i = 0; i < tokens_to_burn; i++) { + _burn(from); + } + } + + // Skip minting for certain addresses to save gas + if (!whitelist[to]) { + uint256 tokens_to_mint = (balanceOf[to] / unit) - + (balanceBeforeReceiver / unit); + for (uint256 i = 0; i < tokens_to_mint; i++) { + _mint(to); + } + } + + emit ERC20Transfer(from, to, amount); + return true; + } + + // Internal utility logic + function _getUnit() internal view returns (uint256) { + return 10 ** decimals; + } + + function _mint(address to) internal virtual { + if (to == address(0)) { + revert InvalidRecipient(); + } + + unchecked { + minted++; + } + + uint256 id = minted; + + if (_ownerOf[id] != address(0)) { + revert AlreadyExists(); + } + + _ownerOf[id] = to; + _owned[to].push(id); + _ownedIndex[id] = _owned[to].length - 1; + + emit Transfer(address(0), to, id); + } + + function _burn(address from) internal virtual { + if (from == address(0)) { + revert InvalidSender(); + } + + uint256 id = _owned[from][_owned[from].length - 1]; + _owned[from].pop(); + delete _ownedIndex[id]; + delete _ownerOf[id]; + delete getApproved[id]; + + emit Transfer(from, address(0), id); + } + + function _setNameSymbol( + string memory _name, + string memory _symbol + ) internal { + name = _name; + symbol = _symbol; + } +} + +contract Pandora is ERC404 { + string public dataURI; + string public baseTokenURI; + + constructor( + address _owner + ) ERC404("Pandora", "PANDORA", 18, 10000, _owner) { + balanceOf[address(this)] = 10000 * 10 ** 18; + whitelist[address(this)] = true; + } + + function setDataURI(string memory _dataURI) public onlyOwner { + dataURI = _dataURI; + } + + function setTokenURI(string memory _tokenURI) public onlyOwner { + baseTokenURI = _tokenURI; + } + + function setNameSymbol( + string memory _name, + string memory _symbol + ) public onlyOwner { + _setNameSymbol(_name, _symbol); + } + + function tokenURI(uint256 id) public view override returns (string memory) { + if (bytes(baseTokenURI).length > 0) { + return string.concat(baseTokenURI, Strings.toString(id)); + } else { + uint8 seed = uint8(bytes1(keccak256(abi.encodePacked(id)))); + string memory image; + string memory color; + + if (seed <= 100) { + image = "1.gif"; + color = "Green"; + } else if (seed <= 160) { + image = "2.gif"; + color = "Blue"; + } else if (seed <= 210) { + image = "3.gif"; + color = "Purple"; + } else if (seed <= 240) { + image = "4.gif"; + color = "Orange"; + } else if (seed <= 255) { + image = "5.gif"; + color = "Red"; + } + + string memory jsonPreImage = string.concat( + string.concat( + string.concat('{"name": "Pandora #', Strings.toString(id)), + '","description":"A collection of 10,000 Replicants enabled by ERC404, an experimental token standard.","external_url":"https://pandora.build","image":"' + ), + string.concat(dataURI, image) + ); + string memory jsonPostImage = string.concat( + '","attributes":[{"trait_type":"Color","value":"', + color + ); + string memory jsonPostTraits = '"}]}'; + + return + string.concat( + "data:application/json;utf8,", + string.concat( + string.concat(jsonPreImage, jsonPostImage), + jsonPostTraits + ) + ); + } + } + + function mint(uint256 _count, address _to) public { + _transfer(address(this), _to, _count * 10 ** 18); + } + + function mint(uint256 _count) external { + mint(_count, msg.sender); + } +} diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 677685c57..eaa0497dc 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -156,6 +156,7 @@ import { PoolAAPositionMover__factory, PoolBorrowAndStake__factory, PoolBorrowAndStake, + Pandora, } from "../types"; import { getACLManager, @@ -1406,6 +1407,7 @@ export const deployAllERC721Tokens = async (verify?: boolean) => { | Land | Meebits | Moonbirds + | Pandora | Contract | StakefishNFTManager; } = {}; @@ -1430,7 +1432,7 @@ export const deployAllERC721Tokens = async (verify?: boolean) => { console.log("contract address is already in db", tokenSymbol); continue; } else if (tokensConfig[tokenSymbol]) { - console.log("contract address is already in db", tokenSymbol); + console.log("contract address is set in market config", tokenSymbol); await insertContractAddressInDb( tokenSymbol, tokensConfig[tokenSymbol], @@ -1488,6 +1490,14 @@ export const deployAllERC721Tokens = async (verify?: boolean) => { continue; } + if (tokenSymbol === ERC721TokenContractId.PANDORA) { + tokens[tokenSymbol] = await deployPandora( + await deployer.getAddress(), + verify + ); + continue; + } + if (tokenSymbol === ERC721TokenContractId.BAYC) { tokens[tokenSymbol] = await deployBAYC( [tokenSymbol, tokenSymbol, "8000", "0"], @@ -2191,6 +2201,17 @@ export const deployUniswapSwapRouter = async ( verify ); +export const deployPandora = async ( + owner: string, + verify?: boolean +): Promise => + withSaveAndVerify( + await getContractFactory("Pandora"), + eContractid.PANDORA, + [owner], + verify + ) as Promise; + export const deployStETH = async (verify?: boolean): Promise => withSaveAndVerify( await getContractFactory("StETHMocked"), diff --git a/helpers/hardhat-constants.ts b/helpers/hardhat-constants.ts index 632e55e1f..8f3715395 100644 --- a/helpers/hardhat-constants.ts +++ b/helpers/hardhat-constants.ts @@ -523,6 +523,7 @@ export const eContractidToContractName = { MockBendDaoLendPool: "MockLendPool", PositionMoverLogic: "PositionMoverLogic", PoolPositionMoverImpl: "PoolPositionMover", + PANDORA: "Pandora", }; export const XTOKEN_TYPE_UPGRADE_WHITELIST = diff --git a/helpers/misc-utils.ts b/helpers/misc-utils.ts index b20e4b44a..912517e4b 100644 --- a/helpers/misc-utils.ts +++ b/helpers/misc-utils.ts @@ -183,8 +183,9 @@ export const isMainnet = (): boolean => isPolygon() || isPolygonZkEVM() || isNeon() || - isParallel(); -isManta() || isLinea(); + isParallel() || + isManta() || + isLinea(); export const safeTransactionServiceUrl = (): string => { return isMoonbeam() diff --git a/helpers/types.ts b/helpers/types.ts index 24c01e6d5..4f2eb3ea8 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -232,6 +232,7 @@ export enum eContractid { NFTDescriptor = "NFTDescriptor", NonfungibleTokenPositionDescriptor = "NonfungibleTokenPositionDescriptor", NonfungiblePositionManager = "NonfungiblePositionManager", + PANDORA = "PANDORA", StETH = "stETH", WStETH = "wstETH", MockAToken = "MockAToken", @@ -671,6 +672,7 @@ export enum ERC721TokenContractId { KODA = "KODA", BLOCKS = "BLOCKS", EXRP = "EXRP", + PANDORA = "PANDORA", } export enum NTokenContractId { diff --git a/market-config/auctionStrategies.ts b/market-config/auctionStrategies.ts index 79df5d8dd..689f0f354 100644 --- a/market-config/auctionStrategies.ts +++ b/market-config/auctionStrategies.ts @@ -231,6 +231,16 @@ export const auctionStrategyEXRP: IAuctionStrategyParams = { tickLength: "900", }; +export const auctionStrategyPandora: IAuctionStrategyParams = { + name: "auctionStrategyPandora", + maxPriceMultiplier: utils.parseUnits("3", 18).toString(), + minExpPriceMultiplier: utils.parseUnits("1.2", 18).toString(), + minPriceMultiplier: utils.parseUnits("0.8", 18).toString(), + stepLinear: utils.parseUnits("0.06875", 18).toString(), + stepExp: utils.parseUnits("0.0285474001559822", 18).toString(), + tickLength: "900", +}; + export const auctionStrategyLinear: IAuctionStrategyParams = { name: "auctionStrategyLinear", maxPriceMultiplier: utils.parseUnits("3", 18).toString(), diff --git a/market-config/index.ts b/market-config/index.ts index 505b91d6a..c9053a754 100644 --- a/market-config/index.ts +++ b/market-config/index.ts @@ -83,6 +83,7 @@ import { strategySTONE, strategyTIA, strategyMANTA, + strategyPandora, } from "./reservesConfigs"; export const CommonConfig: Pick< @@ -353,50 +354,7 @@ export const GoerliConfig: IParaSpaceConfiguration = { }, // RESERVE ASSETS - CONFIG, ASSETS, BORROW RATES, ReservesConfig: { - DAI: strategyDAI, - USDC: strategyUSDC, - USDT: strategyUSDT, - FRAX: strategyFRAX, - WETH: strategyWETH, - APE: strategyAPE, - WBTC: strategyWBTC, - stETH: strategySTETH, - wstETH: strategyWSTETH, - aWETH: strategyAWETH, - bendETH: strategyBENDETH, - cbETH: strategyCBETH, - rETH: strategyRETH, - astETH: strategyASTETH, - awstETH: strategyAWSTETH, - cETH: strategyCETH, - PUNK: strategyPUNK, - BLUR: strategyBLUR, - DOODLE: strategyDoodles, - BAYC: strategyBAYC, - MAYC: strategyMAYC, - WPUNKS: strategyWPunks, - MOONBIRD: strategyMoonbird, - MEEBITS: strategyMeebits, - AZUKI: strategyAzuki, - OTHR: strategyOthr, - CLONEX: strategyClonex, - UniswapV3: strategyUniswapV3, - sAPE: strategySAPE, - cAPE: strategyCAPE, - yAPE: strategyYAPE, - BAKC: strategyBAKC, - SEWER: strategySEWER, - PPG: strategyPudgyPenguins, - SFVLDR: strategyStakefishValidator, - HVMTL: strategyHVMTL, - BEANZ: strategyBEANZ, - DEGODS: strategyDEGODS, - EXP: strategyEXP, - VSL: strategyVSL, - KODA: strategyKODA, - BLOCKS: strategyBLOCKS, - uBAYC: strategyuBAYC, - uPPG: strategyuPPG, + PANDORA: strategyPandora, }, DelegationRegistry: "0x00000000000000447e69651d841bD8D104Bed493", }; @@ -989,7 +947,7 @@ export const MantaConfig: IParaSpaceConfiguration = { USDC: strategyUSDC, TIA: strategyTIA, MANTA: strategyMANTA, - WSTETH: strategyWSTETH, + wstETH: strategyWSTETH, }, Oracle: MantaOracleConfig, }; diff --git a/market-config/mocks.ts b/market-config/mocks.ts index 9a529ffa8..88c76814f 100644 --- a/market-config/mocks.ts +++ b/market-config/mocks.ts @@ -69,6 +69,7 @@ export const MOCK_CHAINLINK_AGGREGATORS_PRICES = { KODA: parseEther("9.5").toString(), BLOCKS: parseEther("9.54").toString(), EXRP: parseEther("0.2").toString(), + PANDORA: parseEther("9").toString(), uBAYC: parseEther("0.000002768").toString(), uPPG: parseEther("0.000000477").toString(), }; @@ -140,6 +141,7 @@ export const MOCK_CHAINLINK_AGGREGATORS_USD_PRICES = { KODA: parseUnits("14154", 8).toString(), BLOCKS: parseUnits("10800", 8).toString(), EXRP: parseUnits("170", 8).toString(), + PANDORA: parseUnits("20000", 8).toString(), uBAYC: parseUnits("0.005", 8).toString(), uPPG: parseUnits("0.004", 8).toString(), }; diff --git a/market-config/reservesConfigs.ts b/market-config/reservesConfigs.ts index ffc1c3ba0..8d015a2ce 100644 --- a/market-config/reservesConfigs.ts +++ b/market-config/reservesConfigs.ts @@ -23,6 +23,7 @@ import { auctionStrategyZero, auctionStrategyEXRP, auctionStrategyUniswapV3, + auctionStrategyPandora, } from "./auctionStrategies"; import { rateStrategyAAVE, @@ -117,6 +118,7 @@ import { timeLockStrategyuBAYC, timeLockStrategyTIA, timeLockStrategyaUSDC, + timeLockStrategyPandora, } from "./timeLockStrategies"; export const strategyDAI: IReserveParams = { @@ -1099,6 +1101,22 @@ export const strategyEXRP: IReserveParams = { supplyCap: "1000", }; +export const strategyPandora: IReserveParams = { + strategy: rateStrategyNFT, + auctionStrategy: auctionStrategyPandora, + timeLockStrategy: timeLockStrategyPandora, + baseLTVAsCollateral: "3000", + liquidationProtocolFeePercentage: "0", + liquidationThreshold: "5000", + liquidationBonus: "10500", + borrowingEnabled: false, + reserveDecimals: "0", + xTokenImpl: eContractid.NTokenImpl, + reserveFactor: "0", + borrowCap: "0", + supplyCap: "1000", +}; + export const strategyuBAYC: IReserveParams = { strategy: rateStrategyBLUR, auctionStrategy: auctionStrategyZero, diff --git a/market-config/timeLockStrategies.ts b/market-config/timeLockStrategies.ts index 29e32aded..3db9050b6 100644 --- a/market-config/timeLockStrategies.ts +++ b/market-config/timeLockStrategies.ts @@ -796,3 +796,15 @@ export const timeLockStrategyEXRP: ITimeLockStrategyParams = { poolPeriodLimit: "100", period: "86400", }; + +export const timeLockStrategyPandora: ITimeLockStrategyParams = { + name: "timeLockStrategyPandora", + minThreshold: "2", + midThreshold: "6", + minWaitTime: "12", + midWaitTime: "7200", + maxWaitTime: "21600", + poolPeriodWaitTime: "600", + poolPeriodLimit: "20", + period: "86400", +};