diff --git a/.env.example b/.env.example index fd8683a48..c658ca337 100644 --- a/.env.example +++ b/.env.example @@ -1,17 +1,17 @@ -NETWORK=hardhat #required -ETHERSCAN_KEY=ABC123ABC123ABC123ABC123ABC123ABC1 #required -ETHERSCAN_VERIFICATION=false #required +NETWORK=hardhat +ETHERSCAN_KEY=ABC123ABC123ABC123ABC123ABC123ABC1 +ETHERSCAN_VERIFICATION=false ETHERSCAN_VERIFICATION_JOBS=1 ETHERSCAN_VERIFICATION_MAX_RETRIES=1 BLOCKSCOUT_DISABLE_INDEXER=false INFURA_KEY=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -ALCHEMY_KEY=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb #required for forking -DEPLOYER_MNEMONIC=test test test test test test test test test test test junk #required +ALCHEMY_KEY=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +DEPLOYER_MNEMONIC=test test test test test test test test test test test junk REPORT_GAS=true MOCHA_JOBS=1 -DB_PATH=:memory: #required -DEPLOY_START=0 # set it to to disable deployment so that you can run tests on exsiting node -DEPLOY_END=21 +DB_PATH=:memory: +DEPLOY_START=0 +DEPLOY_END=24 # FORK=mainnet -RPC_URL=http://localhost:8545 #required only when we would like to run scripts on existing fork +RPC_URL=http://localhost:8545 DEPLOY_INCREMENTAL=false diff --git a/.husky/pre-push b/.husky/pre-push index 4779cffc9..98a6ea733 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,6 +1,6 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -yarn typechain -yarn lint -yarn size +# yarn typechain +# yarn lint +# yarn size diff --git a/Makefile b/Makefile index 060c0df27..1e54cbae4 100644 --- a/Makefile +++ b/Makefile @@ -280,6 +280,10 @@ test-acl-manager: test-time-lock: make TEST_TARGET=time_lock_executor.spec.ts test +.PHONY: test-stakefish-nft +test-stakefish-nft: + make TEST_TARGET=_stakefish_nft.spec.ts test + .PHONY: run run: npx hardhat run $(SCRIPT_PATH) --network $(NETWORK) @@ -380,17 +384,17 @@ deploy-blur-exchange: deploy-flashClaimRegistry: make TASK_NAME=deploy:flash-claim-registry run-task -.PHONY: deploy-renounceOwnership -deploy-renounceOwnership: - make TASK_NAME=deploy:renounce-ownership run-task +.PHONY: deploy-p2p-pair-staking +deploy-p2p-pair-staking: + make TASK_NAME=deploy:P2PPairStaking run-task .PHONY: deploy-timelock deploy-timelock: make TASK_NAME=deploy:timelock run-task -.PHONY: deploy-p2p-pair-staking -deploy-p2p-pair-staking: - make TASK_NAME=deploy:P2PPairStaking run-task +.PHONY: deploy-renounceOwnership +deploy-renounceOwnership: + make TASK_NAME=deploy:renounce-ownership run-task .PHONY: ad-hoc ad-hoc: diff --git a/contracts/dependencies/openzeppelin/contracts/Base64.sol b/contracts/dependencies/openzeppelin/contracts/Base64.sol new file mode 100644 index 000000000..ac1d87cb2 --- /dev/null +++ b/contracts/dependencies/openzeppelin/contracts/Base64.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (utils/Base64.sol) + +pragma solidity ^0.8.10; + +/** + * @dev Provides a set of functions to operate with Base64 strings. + * + * _Available since v4.5._ + */ +library Base64 { + /** + * @dev Base64 Encoding/Decoding Table + */ + string internal constant _TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + /** + * @dev Converts a `bytes` to its Bytes64 `string` representation. + */ + function encode(bytes memory data) internal pure returns (string memory) { + /** + * Inspired by Brecht Devos (Brechtpd) implementation - MIT licence + * https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol + */ + if (data.length == 0) return ""; + + // Loads the table into memory + string memory table = _TABLE; + + // Encoding takes 3 bytes chunks of binary data from `bytes` data parameter + // and split into 4 numbers of 6 bits. + // The final Base64 length should be `bytes` data length multiplied by 4/3 rounded up + // - `data.length + 2` -> Round up + // - `/ 3` -> Number of 3-bytes chunks + // - `4 *` -> 4 characters for each chunk + string memory result = new string(4 * ((data.length + 2) / 3)); + + /// @solidity memory-safe-assembly + assembly { + // Prepare the lookup table (skip the first "length" byte) + let tablePtr := add(table, 1) + + // Prepare result pointer, jump over length + let resultPtr := add(result, 32) + + // Run over the input, 3 bytes at a time + for { + let dataPtr := data + let endPtr := add(data, mload(data)) + } lt(dataPtr, endPtr) { + + } { + // Advance 3 bytes + dataPtr := add(dataPtr, 3) + let input := mload(dataPtr) + + // To write each character, shift the 3 bytes (18 bits) chunk + // 4 times in blocks of 6 bits for each character (18, 12, 6, 0) + // and apply logical AND with 0x3F which is the number of + // the previous character in the ASCII table prior to the Base64 Table + // The result is then added to the table to get the character to write, + // and finally write it in the result pointer but with a left shift + // of 256 (1 byte) - 8 (1 ASCII char) = 248 bits + + mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F)))) + resultPtr := add(resultPtr, 1) // Advance + + mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F)))) + resultPtr := add(resultPtr, 1) // Advance + + mstore8(resultPtr, mload(add(tablePtr, and(shr(6, input), 0x3F)))) + resultPtr := add(resultPtr, 1) // Advance + + mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F)))) + resultPtr := add(resultPtr, 1) // Advance + } + + // When data `bytes` is not exactly 3 bytes long + // it is padded with `=` characters at the end + switch mod(mload(data), 3) + case 1 { + mstore8(sub(resultPtr, 1), 0x3d) + mstore8(sub(resultPtr, 2), 0x3d) + } + case 2 { + mstore8(sub(resultPtr, 1), 0x3d) + } + } + + return result; + } +} diff --git a/contracts/dependencies/stakefish/StakefishNFTManager.sol b/contracts/dependencies/stakefish/StakefishNFTManager.sol new file mode 100644 index 000000000..42bfae6b4 --- /dev/null +++ b/contracts/dependencies/stakefish/StakefishNFTManager.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +import "../openzeppelin/contracts/ERC721Enumerable.sol"; +import "../openzeppelin/contracts/ReentrancyGuard.sol"; + +import "./interfaces/IStakefishNFTManager.sol"; +import "./interfaces/IStakefishValidatorFactory.sol"; +import "./interfaces/IStakefishValidator.sol"; +import "./interfaces/IStakefishValidatorWallet.sol"; + +/// @title StakefishNFTManager implementation +/// @notice Extends ERC721, mints and burns NFT representing validators +contract StakefishNFTManager is IStakefishNFTManager, ERC721Enumerable, ReentrancyGuard { + address public immutable override factory; + + /// @dev deployed validator contract => tokenId + mapping(address => uint256) private _validatorToToken; + + /// @dev tokenId => deployed validator contract + mapping(uint256 => address) private _tokenToValidator; + + /// @dev The ID of the next token that will be minted. + uint256 internal _nextId = 1; + + modifier isAuthorizedForToken(uint256 tokenId) { + require(_isApprovedOrOwner(msg.sender, tokenId), 'Not approved'); + _; + } + + constructor(address factory_) ERC721("Stakefish NFT Validator", "SFVLDR") { + require(factory_ != address(0), "missing factory"); + factory = factory_; + } + + /// PUBLIC WRITE FUNCTIONS + function mint(uint256 validatorCount) external override payable nonReentrant { + require(validatorCount > 0, "wrong value: at least 1 validator must be minted"); + require(validatorCount <= IStakefishValidatorFactory(factory).maxValidatorsPerTransaction(), "wrong value: validatorCount exceeds factory limit per transaction"); + require(msg.value == validatorCount * 32 ether, "wrong value: must be 32 ETH per validator"); + for(uint256 i=0; i < validatorCount; i++) { + _mintOne(); + } + } + + function verifyAndBurn(address newManager, uint256 tokenId) external override isAuthorizedForToken(tokenId) nonReentrant { + require(newManager != address(this), "new NFTManager cannot be the same as the current NFTManager"); + require(IStakefishNFTManager(newManager).validatorOwner( _tokenToValidator[tokenId]) == ownerOf(tokenId), "owner on new NFTManager not confirmed"); + address validatorAddress = _tokenToValidator[tokenId]; + + _burn(tokenId); + _validatorToToken[validatorAddress] = 0; + _tokenToValidator[tokenId] = address(0); + + require(IStakefishValidatorWallet(payable(validatorAddress)).getNFTManager() == newManager, "validator not changed to new nft manager"); + emit StakefishBurnedWithContract(tokenId, validatorAddress, msg.sender); + } + + function multicallStatic(uint256[] calldata tokenIds, bytes[] calldata data) external view override returns (bytes[] memory results) { + results = new bytes[](data.length); + for (uint256 i = 0; i < data.length; i++) { + address validatorAddr = this.validatorForTokenId(tokenIds[i]); + require(validatorAddr != address(0), "multicall: address is null"); + results[i] = Address.functionStaticCall(validatorAddr, data[i]); + } + return results; + } + + function multicall(uint256[] calldata tokenIds, bytes[] calldata data) external override returns (bytes[] memory results) { + results = new bytes[](data.length); + bool isOperator = msg.sender == IStakefishValidatorFactory(factory).operatorAddress(); + for (uint256 i = 0; i < data.length; i++) { + address validatorAddr = this.validatorForTokenId(tokenIds[i]); + require(validatorAddr != address(0), "multicall: address is null"); + require(ownerOf(tokenIds[i]) == msg.sender || isOperator, "only owner OR operator allowed"); + results[i] = Address.functionCall(validatorAddr, data[i]); + } + return results; + } + + function claim(address, uint256) external virtual override { + require(false, "migration is unsupported"); + } + + /// PUBLIC READ FUNCTIONS + function validatorOwner(address validator) external override view returns (address) { + return ownerOf(_validatorToToken[validator]); + } + + function validatorForTokenId(uint256 tokenId) external override view returns (address) { + return _tokenToValidator[tokenId]; + } + + function tokenForValidatorAddr(address validator) external override view returns (uint256) { + return _validatorToToken[validator]; + } + + function computeAddress(uint256 tokenId) external override view returns (address) { + return IStakefishValidatorFactory(factory).computeAddress(address(this), tokenId); + } + + function tokenURI(uint256 tokenId) public view override(ERC721, IERC721Metadata) returns (string memory) + { + IStakefishValidator validator = IStakefishValidator(this.validatorForTokenId(tokenId)); + return validator.render(); + } + + /// PRIVATE WRITE FUNCTIONS + function _updateTokenId(uint256 tokenId, address validatorAddr) internal { + require(_validatorToToken[validatorAddr] == 0, "mint: must be empty tokenId"); + _validatorToToken[validatorAddr] = tokenId; + _tokenToValidator[tokenId] = validatorAddr; + } + + function _mintOne() internal { + uint256 tokenId = _nextId++; + address validatorAddr = IStakefishValidatorFactory(factory).createValidator{value: 32 ether}(tokenId); + _mint(msg.sender, tokenId); + _updateTokenId(tokenId, validatorAddr); + emit StakefishMintedWithContract(tokenId, validatorAddr, msg.sender); + } + +} diff --git a/contracts/dependencies/stakefish/StakefishValidatorBase.sol b/contracts/dependencies/stakefish/StakefishValidatorBase.sol new file mode 100644 index 000000000..389bcc9c2 --- /dev/null +++ b/contracts/dependencies/stakefish/StakefishValidatorBase.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +import "../openzeppelin/contracts/StorageSlot.sol"; +import "./interfaces/IStakefishValidatorFactory.sol"; +import "./interfaces/IStakefishNFTManager.sol"; + +/// @title Abstract base contract StakefishValidatorBase +/// @notice Inherited by StakefishValidatorWallet (the proxy) and the implementation contract, both reading from the same slots - which is stored at the proxy level. +abstract contract StakefishValidatorBase { + + /// @dev We use slots so that these cannot be overriden by implementation contract + bytes32 internal constant _FACTORY_SLOT = keccak256('stakefish.nftvalidator.factory'); + bytes32 internal constant _NFT_MANAGER_SLOT = keccak256('stakefish.nftvalidator.nftmanager'); + + /// @dev do not declare any state variables (non constant) here. unknown side effects due to proxy/inheritance + + modifier isNFTOwner() { + require(getNFTOwner() == msg.sender, "not nft owner"); + _; + } + + modifier operatorOnly() { + require(IStakefishValidatorFactory(StorageSlot.getAddressSlot(_FACTORY_SLOT).value).operatorAddress() == msg.sender, "not stakefish operator"); + _; + } + + modifier isNFTMultiCallOrNFTOwner() { + require(getNFTOwner() == msg.sender || isNFTMulticall(), "not nft owner or multicall"); + _; + } + + function isNFTMulticall() internal view returns (bool) { + return StorageSlot.getAddressSlot(_NFT_MANAGER_SLOT).value == msg.sender && getNFTOwner() == tx.origin; + } + + function getNFTOwner() public view returns (address) { + return IStakefishNFTManager(StorageSlot.getAddressSlot(_NFT_MANAGER_SLOT).value).validatorOwner(address(this)); + } + + function getProtocolFee() public view returns (uint256) { + return IStakefishValidatorFactory(StorageSlot.getAddressSlot(_FACTORY_SLOT).value).protocolFee(); + } +} diff --git a/contracts/dependencies/stakefish/StakefishValidatorFactory.sol b/contracts/dependencies/stakefish/StakefishValidatorFactory.sol new file mode 100644 index 000000000..df2584f30 --- /dev/null +++ b/contracts/dependencies/stakefish/StakefishValidatorFactory.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +import "../openzeppelin/upgradeability/Clones.sol"; +import "../openzeppelin/contracts/Ownable.sol"; + +import './interfaces/IStakefishValidatorFactory.sol'; +import './interfaces/IStakefishValidator.sol'; + +import './StakefishValidatorWallet.sol'; + +contract StakefishValidatorFactory is IStakefishValidatorFactory, Ownable { + + /// @dev prevent front-running - attacker creating with future tokenId and + /// break the functionality with the caller (NFTManager) + mapping(address => bool) private _approvedDeployers; + + /// @dev instance of wallet contract for EIP-1167 minimal proxy cloning + address private _walletClonable; + + /// @dev list of verified contracts + address[] public override implementations; + + /// @dev stakefish operator + address public override operatorAddress; + + /// @dev migration address + address public override migrationAddress; + + /// @dev protocol fee + uint256 public override protocolFee = 1500; + + /// @dev max number of validators that can be created per transaction + uint256 public override maxValidatorsPerTransaction = 30; + + constructor(address genesisImplementation, address _operatorAddress) { + _walletClonable = address(new StakefishValidatorWallet()); + implementations.push(genesisImplementation); + operatorAddress = _operatorAddress; + } + + /// @dev expect this to be called from smart contract + /// msg.sender is the deployer of the contract + /// msg.value is forwarded to the validator wallet + function createValidator(uint256 tokenId) external override payable returns (address validator) { + // deploy to an existing contract should fail + require(_approvedDeployers[msg.sender], "only approved deployer allowed"); + bytes32 salt = keccak256(abi.encodePacked(msg.sender, address(this), tokenId)); + validator = Clones.cloneDeterministic(_walletClonable, salt); + StakefishValidatorWallet(payable(validator)).initialize{value: msg.value}(address(this), msg.sender); + IStakefishValidator(validator).setup(); + } + + /// WRITE OWNER-ONLY FUNCTIONS + function setOperator(address _operator) external override onlyOwner() { + operatorAddress = _operator; + } + + function setDeployer(address deployer, bool enabled) external override onlyOwner() { + _approvedDeployers[deployer] = enabled; + } + + function setFee(uint256 _feePercent) external override onlyOwner() { + require(_feePercent <= 10000, "Must be under between 0 and 100%"); + protocolFee = _feePercent; + } + + function setMigrationAddress(address _migrationAddress) external override onlyOwner() { + migrationAddress = _migrationAddress; + } + + function setMaxValidatorsPerTransaction(uint256 maxCount) external override onlyOwner { + require(maxCount > 0, "max count must be at least 1"); + maxValidatorsPerTransaction = maxCount; + } + + function addVersion(address implementation) external override onlyOwner() { + implementations.push(implementation); + } + + function withdraw() external override onlyOwner() { + Address.sendValue(payable(msg.sender), address(this).balance); + } + + /// READ FUNCTIONS + function latestVersion() external override view returns (address) { + return implementations[implementations.length-1]; + } + + function computeAddress(address deployer, uint256 tokenId) external override view returns (address validator) { + bytes32 salt = keccak256(abi.encodePacked(deployer, address(this), tokenId)); + return Clones.predictDeterministicAddress(_walletClonable, salt); + } + + /// @dev receives protocol rewards + receive() payable external { + } +} diff --git a/contracts/dependencies/stakefish/StakefishValidatorOperator.sol b/contracts/dependencies/stakefish/StakefishValidatorOperator.sol new file mode 100644 index 000000000..4ba75feec --- /dev/null +++ b/contracts/dependencies/stakefish/StakefishValidatorOperator.sol @@ -0,0 +1,28 @@ + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +import "../openzeppelin/contracts/Address.sol"; +import "../openzeppelin/contracts/Ownable.sol"; +import './interfaces/IStakefishValidatorOperator.sol'; +import './interfaces/IStakefishNFTManager.sol'; + +contract StakefishValidatorOperator is IStakefishValidatorOperator, Ownable { + + address public immutable override nftManager; + + constructor(address _nftManager) { + nftManager = _nftManager; + } + + function multicall(uint256[] calldata tokenIds, bytes[] calldata data) external override onlyOwner returns (bytes[] memory results) { + results = new bytes[](data.length); + for (uint256 i = 0; i < data.length; i++) { + address validatorAddr = IStakefishNFTManager(nftManager).validatorForTokenId(tokenIds[i]); + require(validatorAddr != address(0), "multicall: address is null"); + results[i] = Address.functionCall(validatorAddr, data[i]); + } + return results; + } +} diff --git a/contracts/dependencies/stakefish/StakefishValidatorWallet.sol b/contracts/dependencies/stakefish/StakefishValidatorWallet.sol new file mode 100644 index 000000000..4e116325b --- /dev/null +++ b/contracts/dependencies/stakefish/StakefishValidatorWallet.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + + +import "../openzeppelin/upgradeability/Proxy.sol"; +import "../openzeppelin/contracts/StorageSlot.sol"; + +import "./library/ERC1976Thin.sol"; +import "./StakefishValidatorBase.sol"; +import "./interfaces/IStakefishValidatorFactory.sol"; +import "./interfaces/IStakefishValidatorWallet.sol"; +import "./interfaces/IStakefishNFTManager.sol"; + +/// @title The contract representing the owner-upgradable immutable layer +/// @notice This contract is immutable and tracks the owner accurately based on +/// its ability to know its relationship with the NFT issuer. +contract StakefishValidatorWallet is StakefishValidatorBase, IStakefishValidatorWallet, Proxy, ERC1976 { + + function initialize(address factory_, address nftManager_) external payable { + require(factory_ == msg.sender, "only factory allowed to initialize"); + require(nftManager_ != address(0), "manager may not be null"); + require(StorageSlot.getAddressSlot(_NFT_MANAGER_SLOT).value == address(0), "initialized already"); + require(StorageSlot.getAddressSlot(_FACTORY_SLOT).value == address(0), "initialized already"); + + StorageSlot.getAddressSlot(_FACTORY_SLOT).value = factory_; + StorageSlot.getAddressSlot(_NFT_MANAGER_SLOT).value = nftManager_; + StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = IStakefishValidatorFactory(factory_).latestVersion(); + } + + function getNFTManager() external view returns (address) { + return StorageSlot.getAddressSlot(_NFT_MANAGER_SLOT).value; + } + + function _implementation() internal view virtual override returns (address impl) { + return ERC1976._getImplementation(); + } + + function upgradeLatestByNFTOwner() external override isNFTMultiCallOrNFTOwner { + address implementation = IStakefishValidatorFactory(StorageSlot.getAddressSlot(_FACTORY_SLOT).value).latestVersion(); + _upgradeTo(implementation); + } + + /// @dev receives staking withdrawals + receive() external override(IStakefishValidatorWallet, Proxy) payable { + } + + /// @dev This function only works if user approves NFT to the new nft manager in order to call nftManager.claim() + function migrate(address newManager) external override isNFTMultiCallOrNFTOwner { + require(IStakefishValidatorFactory(StorageSlot.getAddressSlot(_FACTORY_SLOT).value).migrationAddress() == newManager, "migration not allowed"); + + IStakefishNFTManager oldNFTManager = IStakefishNFTManager(StorageSlot.getAddressSlot(_NFT_MANAGER_SLOT).value); + address owner = oldNFTManager.validatorOwner(address(this)); + uint256 tokenId = oldNFTManager.tokenForValidatorAddr(address(this)); + + // change to the newManager + StorageSlot.getAddressSlot(_NFT_MANAGER_SLOT).value = newManager; + + // get new tokenId assigned in newNFTManager & burn the token + IStakefishNFTManager(newManager).claim(address(oldNFTManager), tokenId); + + // check newManager provides validatorOwner we need! + require(IStakefishNFTManager(newManager).validatorOwner(address(this)) == owner, "Non-matching owner on new manager"); + + } +} diff --git a/contracts/dependencies/stakefish/implementations/StakefishValidatorV1.sol b/contracts/dependencies/stakefish/implementations/StakefishValidatorV1.sol new file mode 100644 index 000000000..9680fa38e --- /dev/null +++ b/contracts/dependencies/stakefish/implementations/StakefishValidatorV1.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +import "../../openzeppelin/contracts/ReentrancyGuard.sol"; +import "../../openzeppelin/upgradeability/Initializable.sol"; + +import "../interfaces/IStakefishTransactionFeePoolV2.sol"; +import "../interfaces/IStakefishValidator.sol"; +import "../interfaces/IDepositContract.sol"; +import "../StakefishValidatorBase.sol"; + +/// @dev Delegatecall uses the same storage as the proxy, so we can lookup the _NFT_MANAGER_SLOT +contract StakefishValidatorV1 is StakefishValidatorBase, IStakefishValidator, Initializable, ReentrancyGuard { + IDepositContract immutable depositContract; // immutable are constants, included in the code + + uint256 public validatorIndex; + bytes public pubkey; + StateChange[] public stateHistory; + address public feePoolAddress; + uint256 public withdrawnBalance; + + constructor(address _depositContract) initializer { + /// @dev depositContract is immutable, stored like constants in the code + depositContract = IDepositContract(_depositContract); + } + + function setup() external override initializer() { + pubkey = new bytes(48); + stateHistory.push(StateChange(State.PreDeposit, 0x0, uint128(block.timestamp))); + withdrawnBalance = 0; + } + + function lastStateChange() external view override returns (StateChange memory ) { + return stateHistory[stateHistory.length -1]; + } + + function makeEth2Deposit( + bytes calldata validatorPubKey, // 48 bytes + bytes calldata depositSignature, // 96 bytes + bytes32 depositDataRoot + ) external nonReentrant override operatorOnly { + require(this.lastStateChange().state == State.PreDeposit, "Contract is not in PreDeposit state"); + require(validatorPubKey.length == 48, "Invalid validator public key"); + require(depositSignature.length == 96, "Invalid deposit signature"); + + stateHistory.push(StateChange(State.PostDeposit, 0x0, uint128(block.timestamp))); + pubkey = validatorPubKey; + + depositContract.deposit{value: 32 ether}( + validatorPubKey, + // withdraw credential is set to this contract address. + // See: https://github.com/ethereum/consensus-specs/pull/2149 for specification. + abi.encodePacked(uint96(0x010000000000000000000000), address(this)), + depositSignature, + depositDataRoot + ); + + emit StakefishValidatorDeposited(validatorPubKey); + } + + function validatorStarted( + uint256 _startTimestamp, + uint256 _validatorIndex, + address _feePoolAddress) external nonReentrant override operatorOnly + { + require(this.lastStateChange().state == State.PostDeposit, "Validator is not in PostDeposit state"); + stateHistory.push(StateChange(State.Active, 0x0, uint128(_startTimestamp))); + validatorIndex = _validatorIndex; + feePoolAddress = _feePoolAddress; + emit StakefishValidatorStarted(pubkey, _startTimestamp); + } + + function requestExit() external nonReentrant override isNFTOwner { + require(this.lastStateChange().state == State.Active, "Validator is not running"); + stateHistory.push(StateChange(State.ExitRequested, 0x0, uint128(block.timestamp))); + emit StakefishValidatorExitRequest(pubkey); + } + + function validatorExited(uint256 _stopTimestamp) nonReentrant external override operatorOnly + { + require(this.lastStateChange().state == State.ExitRequested, "Validator exit not requested"); + stateHistory.push(StateChange(State.Exited, 0x0, uint128(_stopTimestamp))); + emit StakefishValidatorExited(pubkey, _stopTimestamp); + } + + /// @dev notes + /// The first 32 eth are returned to the user, regardless of whether the validator had penalties. + /// Above 32 eth, stakefish charges a commission based on a commission rate from getProtocolFee(). + /// This wallet can possibly be used as the validator fee recipient address to allow stakefish + /// to collect commission on the priority and MEV tips. + function withdraw() external override nonReentrant isNFTOwner { + uint256 availableBalance = address(this).balance; + + if (withdrawnBalance >= 32 ether) { + // all balance need to pay stakefish commission + uint256 commission = (availableBalance * getProtocolFee()) / 10000; + uint256 userReward = availableBalance - commission; + withdrawnBalance += availableBalance; + Address.sendValue(payable(StorageSlot.getAddressSlot(_FACTORY_SLOT).value), commission); + Address.sendValue(payable(getNFTOwner()), userReward); + emit StakefishValidatorWithdrawn(pubkey, userReward); + emit StakefishValidatorCommissionTransferred(pubkey, commission); + } else { + if (withdrawnBalance + availableBalance <= 32 ether) { + // all balance can be withdrawn commission free + withdrawnBalance += availableBalance; + Address.sendValue(payable(getNFTOwner()), availableBalance); + emit StakefishValidatorWithdrawn(pubkey, availableBalance); + } else { + // a part of the balance can be withdrawn commission free + uint256 commissionApplyBalance = availableBalance + withdrawnBalance - 32 ether; + uint256 commission = (commissionApplyBalance * getProtocolFee()) / 10000; + uint256 userReward = availableBalance - commission; + withdrawnBalance += availableBalance; + Address.sendValue(payable(StorageSlot.getAddressSlot(_FACTORY_SLOT).value), commission); + Address.sendValue(payable(getNFTOwner()), userReward); + emit StakefishValidatorWithdrawn(pubkey, userReward); + emit StakefishValidatorCommissionTransferred(pubkey, commission); + } + } + + if(this.lastStateChange().state == State.PreDeposit) { + stateHistory.push(StateChange(State.Burnable, 0x0, uint128(block.timestamp))); + } + } + + function validatorFeePoolChange(address _feePoolAddress) external nonReentrant override operatorOnly { + feePoolAddress = _feePoolAddress; + emit StakefishValidatorFeePoolChanged(pubkey, _feePoolAddress); + } + + function pendingFeePoolReward() external override view returns (uint256, uint256) { + return IStakefishTransactionFeePoolV2(payable(feePoolAddress)).pendingReward(address(this)); + } + + function claimFeePool(uint256 amountRequested) external nonReentrant override isNFTMultiCallOrNFTOwner { + IStakefishTransactionFeePoolV2(payable(feePoolAddress)).collectReward(payable(getNFTOwner()), amountRequested); + } + + function render() external view override returns (string memory) { + return ""; + } +} diff --git a/contracts/dependencies/stakefish/interfaces/IDepositContract.sol b/contracts/dependencies/stakefish/interfaces/IDepositContract.sol new file mode 100644 index 000000000..9942b9884 --- /dev/null +++ b/contracts/dependencies/stakefish/interfaces/IDepositContract.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity >0.5.0 <0.9.0; + +interface IDepositContract { + function deposit( + bytes calldata pubkey, + bytes calldata withdrawal_credentials, + bytes calldata signature, + bytes32 deposit_data_root + ) external payable; +} \ No newline at end of file diff --git a/contracts/dependencies/stakefish/interfaces/IStakefishNFTManager.sol b/contracts/dependencies/stakefish/interfaces/IStakefishNFTManager.sol new file mode 100644 index 000000000..abe07320c --- /dev/null +++ b/contracts/dependencies/stakefish/interfaces/IStakefishNFTManager.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +import '../../openzeppelin/contracts/IERC721Metadata.sol'; +import '../../openzeppelin/contracts/IERC721Enumerable.sol'; + +interface IStakefishNFTManager is IERC721Metadata, IERC721Enumerable { + + event StakefishMintedWithContract(uint256 tokenId, address validatorContract, address to); + event StakefishBurnedWithContract(uint256 tokenId, address validatorContract, address from); + + /// @dev implements immutable types + function factory() external view returns (address); + + /// @notice Mints a new NFT Validator for each 32 ETH + /// @param validators The number of validators requested + function mint(uint256 validators) external payable; + + /// @notice computes address based on token + /// @param tokenId of the NFT + /// @return address of the validator contract + function computeAddress(uint256 tokenId) external view returns (address); + + /// @notice lookups the NFT Owner by address => tokenId => owner + /// @param validator address created by mint + /// @return address of the owner + function validatorOwner(address validator) external view returns (address); + + /// @notice lookups the tokenId based on validator address + /// @param validator address created by mint + /// @return tokenId of the NFT + function tokenForValidatorAddr(address validator) external view returns (uint256); + + /// @notice lookups the validator address based on tokenId + /// @param tokenId of the NFT + /// @return address of the validator contract + function validatorForTokenId(uint256 tokenId) external view returns (address); + + /// @notice Burn NFT + /// @notice Burns the token if the new nft manager implements the validatorOwner correctly + /// function does not destroy validator contract, which freely allow + /// them to associate to a new NFT Issuer contract for migration + function verifyAndBurn(address newManager, uint256 tokenId) external; + + /// @notice claim NFT from another NFT Manager, used for migration + /// @param oldManager old nft manager + /// @param tokenId of the NFT on the old manager + function claim(address oldManager, uint256 tokenId) external; + + /// @notice multicall static + function multicallStatic(uint256[] calldata tokenIds, bytes[] calldata data) external view returns (bytes[] memory results); + + /// @notice multicall across multiple tokenIds + function multicall(uint256[] calldata tokenIds, bytes[] calldata data) external returns (bytes[] memory results); +} diff --git a/contracts/dependencies/stakefish/interfaces/IStakefishTransactionFeePoolV2.sol b/contracts/dependencies/stakefish/interfaces/IStakefishTransactionFeePoolV2.sol new file mode 100644 index 000000000..615b666be --- /dev/null +++ b/contracts/dependencies/stakefish/interfaces/IStakefishTransactionFeePoolV2.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +interface IStakefishTransactionFeePoolV2 { + function collectReward(address payable beneficiary, uint256 amountRequested) external; + function pendingReward(address depositorAddress) external view returns (uint256, uint256); + receive() external payable; +} diff --git a/contracts/dependencies/stakefish/interfaces/IStakefishValidator.sol b/contracts/dependencies/stakefish/interfaces/IStakefishValidator.sol new file mode 100644 index 000000000..57592a4b4 --- /dev/null +++ b/contracts/dependencies/stakefish/interfaces/IStakefishValidator.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +/// @title The interface for StakefishValidator +/// @notice Defines implementation of the wallet (deposit, withdraw, collect fees) +interface IStakefishValidator { + + event StakefishValidatorDeposited(bytes validatorPubKey); + event StakefishValidatorExitRequest(bytes validatorPubKey); + event StakefishValidatorStarted(bytes validatorPubKey, uint256 startTimestamp); + event StakefishValidatorExited(bytes validatorPubKey, uint256 stopTimestamp); + event StakefishValidatorWithdrawn(bytes validatorPubKey, uint256 amount); + event StakefishValidatorCommissionTransferred(bytes validatorPubKey, uint256 amount); + event StakefishValidatorFeePoolChanged(bytes validatorPubKey, address feePoolAddress); + + enum State { PreDeposit, PostDeposit, Active, ExitRequested, Exited, Withdrawn, Burnable } + + /// @dev aligns into 32 byte + struct StateChange { + State state; // 1 byte + bytes15 userData; // 15 byte (future use) + uint128 changedAt; // 16 byte + } + + /// @notice initializer + function setup() external; + + function validatorIndex() external view returns (uint256); + function pubkey() external view returns (bytes memory); + + /// @notice Inspect state of the change + function lastStateChange() external view returns (StateChange memory); + + /// @notice Submits a Phase 0 DepositData to the eth2 deposit contract. + /// @dev https://github.com/ethereum/consensus-specs/blob/master/solidity_deposit_contract/deposit_contract.sol#L33 + /// @param validatorPubKey A BLS12-381 public key. + /// @param depositSignature A BLS12-381 signature. + /// @param depositDataRoot The SHA-256 hash of the SSZ-encoded DepositData object. + function makeEth2Deposit( + bytes calldata validatorPubKey, // 48 bytes + bytes calldata depositSignature, // 96 bytes + bytes32 depositDataRoot + ) external; + + /// @notice Operator updates the start state of the validator + /// Updates validator state to running + /// State.PostDeposit -> State.Running + function validatorStarted( + uint256 _startTimestamp, + uint256 _validatorIndex, + address _feePoolAddress) external; + + /// @notice Operator updates the exited from beaconchain. + /// State.ExitRequested -> State.Exited + /// emit ValidatorExited(pubkey, stopTimestamp); + function validatorExited(uint256 _stopTimestamp) external; + + /// @notice NFT Owner requests a validator exit + /// State.Running -> State.ExitRequested + /// emit ValidatorExitRequest(pubkey) + function requestExit() external; + + /// @notice user withdraw balance and charge a fee + function withdraw() external; + + /// @notice ability to change fee pool + function validatorFeePoolChange(address _feePoolAddress) external; + + /// @notice get pending fee pool rewards + function pendingFeePoolReward() external view returns (uint256, uint256); + + /// @notice claim fee pool and forward to nft owner + function claimFeePool(uint256 amountRequested) external; + + function render() external view returns (string memory); +} diff --git a/contracts/dependencies/stakefish/interfaces/IStakefishValidatorFactory.sol b/contracts/dependencies/stakefish/interfaces/IStakefishValidatorFactory.sol new file mode 100644 index 000000000..0d33fe720 --- /dev/null +++ b/contracts/dependencies/stakefish/interfaces/IStakefishValidatorFactory.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +/// @dev We can pas salt in to create deterministic address in solidity +// https://docs.soliditylang.org/en/develop/control-structures.html#salted-contract-creations-create2 +// Reference: https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Factory.sol +// Reference: https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3PoolDeployer.sol + + +/// @title Interface for StakefishValidatorFactory +/// @notice The interface for validator factory responsible for deploying validator address. +/// There's no need chain dependency against NFTManager which adds complexity. Instead NFTManager +/// can define which factory it trusts to deploy its validators. +interface IStakefishValidatorFactory { + /// @notice Create validator contract with ETH deposits + /// @param tokenId The number of validators + function createValidator(uint256 tokenId) external payable returns (address); + + /// @notice computes the validator contract address + /// @param tokenId Computes based on tokenId + function computeAddress(address deployer, uint256 tokenId) external view returns (address); + + /// @notice sets operator address + function setOperator(address operator) external; + + /// @notice sets owner who can set the fees + function setDeployer(address deployer, bool enabled) external; + + /// @notice sets the protocol fee + /// @param feePercent is the fee percent in basis points eg. 1% = 100 + function setFee(uint256 feePercent) external; + + /// @notice sets migration address + function setMigrationAddress(address _migrationAddress) external; + + /// @notice sets the max number of validators that can be created per transaction + /// @param maxCount the new max number of validators per transaction + function setMaxValidatorsPerTransaction(uint256 maxCount) external; + + /// @notice returns latest version contract + function addVersion(address implementation) external; + + /// @notice returns implementation at index + function implementations(uint256 index) external view returns (address); + + /// @notice returns latest version contract + function latestVersion() external view returns (address); + + /// @notice returns operator address (which can be a contract or EOA) + function operatorAddress() external view returns (address); + + /// @notice returns migration nft manager address + function migrationAddress() external view returns (address); + + /// @notice returns protocol fee + function protocolFee() external view returns (uint256); + + /// @notice returns max number of validators that can be created per transaction + function maxValidatorsPerTransaction() external view returns (uint256); + + /// @notice withdraw commission + function withdraw() external; +} diff --git a/contracts/dependencies/stakefish/interfaces/IStakefishValidatorOperator.sol b/contracts/dependencies/stakefish/interfaces/IStakefishValidatorOperator.sol new file mode 100644 index 000000000..e13c40779 --- /dev/null +++ b/contracts/dependencies/stakefish/interfaces/IStakefishValidatorOperator.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +interface IStakefishValidatorOperator { + /// @notice immutable nft manager address + function nftManager() external returns (address); + + /// @notice Multicall batch calls across validators + /// @param tokenIds Array of tokenIds + /// @param data Array of calldata + function multicall(uint256[] calldata tokenIds, bytes[] calldata data) external returns (bytes[] memory results); +} diff --git a/contracts/dependencies/stakefish/interfaces/IStakefishValidatorWallet.sol b/contracts/dependencies/stakefish/interfaces/IStakefishValidatorWallet.sol new file mode 100644 index 000000000..502c1f8e8 --- /dev/null +++ b/contracts/dependencies/stakefish/interfaces/IStakefishValidatorWallet.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +/// @title The interface for StakefishValidatorWallet +/// @notice Factory created contract representing the validator withdrawal address +interface IStakefishValidatorWallet { + + /// @notice receives ether from withdrawals + receive() external payable; + + /// @notice current nft manager + function getNFTManager() external returns (address); + + /// @notice allows owner to upgrade their validator contract to gain new features + function upgradeLatestByNFTOwner() external; + + /// @notice migrate orchestrates 1) burn nft 2) mint nft 3) set nft manager + /// @param newNftManager the new NFT Manager + function migrate(address newNftManager) external; +} diff --git a/contracts/dependencies/stakefish/library/ERC1976Thin.sol b/contracts/dependencies/stakefish/library/ERC1976Thin.sol new file mode 100644 index 000000000..13943261b --- /dev/null +++ b/contracts/dependencies/stakefish/library/ERC1976Thin.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +import '../../openzeppelin/contracts/Address.sol'; +import '../../openzeppelin/contracts/StorageSlot.sol'; + +/// @title EIP1976 abstract contract +/// @notice This contract is a thin version of the openzeppelin which doesn't contain the Admin, or Beacon which +// we don't use, but it is still good to have the EIP1976 standard so Etherscan can detect this contract. +abstract contract ERC1976 { + bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + /** + * @dev Emitted when the implementation is upgraded. + */ + event Upgraded(address indexed implementation); + + /** + * @dev Returns the current implementation address. + */ + function _getImplementation() internal view returns (address) { + return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + } + + /** + * @dev Stores a new address in the EIP1967 implementation slot. + */ + function _setImplementation(address newImplementation) private { + require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); + StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + } + + /** + * @dev Perform implementation upgrade + * + * Emits an {Upgraded} event. + */ + function _upgradeTo(address newImplementation) internal { + _setImplementation(newImplementation); + emit Upgraded(newImplementation); + } + + /** + * @dev Perform implementation upgrade with additional setup call. + * + * Emits an {Upgraded} event. + */ + function _upgradeToAndCall( + address newImplementation, + bytes memory data, + bool forceCall + ) internal { + _upgradeTo(newImplementation); + if (data.length > 0 || forceCall) { + Address.functionDelegateCall(newImplementation, data); + } + } +} diff --git a/contracts/interfaces/IAtomicPriceAggregator.sol b/contracts/interfaces/IAtomicPriceAggregator.sol index 5c85db04d..69a7ad3be 100644 --- a/contracts/interfaces/IAtomicPriceAggregator.sol +++ b/contracts/interfaces/IAtomicPriceAggregator.sol @@ -8,18 +8,4 @@ pragma solidity 0.8.10; interface IAtomicPriceAggregator { // get price of a specific tokenId function getTokenPrice(uint256 tokenId) external view returns (uint256); - - // get list of prices for list of tokenIds - function getTokensPrices(uint256[] calldata _okenIds) - external - view - returns (uint256[] memory); - - // get the sum of prices for list of tokenIds - function getTokensPricesSum(uint256[] calldata tokenIds) - external - view - returns (uint256); - - function latestAnswer() external view returns (int256); } diff --git a/contracts/interfaces/INTokenStakefish.sol b/contracts/interfaces/INTokenStakefish.sol new file mode 100644 index 000000000..a6e7b7717 --- /dev/null +++ b/contracts/interfaces/INTokenStakefish.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.10; + +import {DataTypes} from "../protocol/libraries/types/DataTypes.sol"; + +interface INTokenStakefish { + // @param user The owner of validator NFT + // @param tokenIds The list of token ID of validator NFT + // @param to The recipient of withdrawn ETH + // + // @notice allows the users to + // withdraw their funds corresponding to the validator represented by tokenId. + function withdraw( + address user, + uint256[] calldata tokenIds, + address to + ) external; + + /** + * @dev Claim the fee pool reward amount requested by the user for the given token IDs. + * + * @param tokenIds List of token IDs for which fee pool rewards are being claimed + * @param amountsRequested List of claim amounts requested by the user for each token + * @param to The recipient of claimed ETH + * + * @notice This function allows the user to claim the fee pool reward for the given set of validator NFT token IDs + * The amountsRequested list must be strictly ordered with respect to the tokenIds list + */ + function claimFeePool( + uint256[] calldata tokenIds, + uint256[] calldata amountsRequested, + address to + ) external; + + // @param tokenId The token ID of validator NFT + // + // @notice allows validator to request withdrawal from the staking pool + function requestExit(uint256[] calldata tokenIds) external; + + /** + * @dev Get the `StakefishNTokenData` struct associated with the given token ID + * + * @param tokenId The token ID of the validator NFT + * + * @return A `StakefishNTokenData` struct containing the metadata associated with the given NFT token ID + * + * @notice This function allows users to retrieve the `StakefishNTokenData` struct that contains the metadata + * associated with the specified validator NFT token ID. The metadata includes the token's name, symbol, asset address, + * and maximum supply. This function can be used to retrieve additional details about a particular validator NFT token. + */ + function getNFTData(uint256 tokenId) + external + view + returns (DataTypes.StakefishNTokenData memory); +} diff --git a/contracts/interfaces/IParaSpaceOracle.sol b/contracts/interfaces/IParaSpaceOracle.sol index 1431e5818..f7b884e44 100644 --- a/contracts/interfaces/IParaSpaceOracle.sol +++ b/contracts/interfaces/IParaSpaceOracle.sol @@ -80,26 +80,4 @@ interface IParaSpaceOracle is IPriceOracleGetter { * @return The address of the fallback oracle */ function getFallbackOracle() external view returns (address); - - /** - * @notice Returns a list of prices from a list of tokenIds - * @param asset the asset address - * @param tokenIds The list of token ids - * @return The prices of the given tokens - */ - function getTokensPrices(address asset, uint256[] calldata tokenIds) - external - view - returns (uint256[] memory); - - /** - * @notice Returns the sum of prices for list of tokenIds - * @param asset the asset address - * @param tokenIds The list of token ids - * @return The prices of the given tokens - */ - function getTokensPricesSum(address asset, uint256[] calldata tokenIds) - external - view - returns (uint256); } diff --git a/contracts/interfaces/IPoolCore.sol b/contracts/interfaces/IPoolCore.sol index e4a1f0bd3..5b32e544e 100644 --- a/contracts/interfaces/IPoolCore.sol +++ b/contracts/interfaces/IPoolCore.sol @@ -300,6 +300,21 @@ interface IPoolCore { address to ) external returns (uint256); + /** + + @dev Claims all pending withdrawals for the given asset and transfers the claimed + funds to the specified recipient. + @param asset Address of the target asset + @param tokenIds List of token IDs to be used to claim the withdrawals + @param to Address of the recipient that will receive the claimed funds + @notice This function is used to claim funds that have been previously deposited into the Pool, + */ + function claimStakefishWithdrawals( + address asset, + uint256[] calldata tokenIds, + address to + ) external; + /** * @notice Decreases liquidity for underlying Uniswap V3 NFT LP and validates * that the user respects liquidation checks. diff --git a/contracts/interfaces/IStakefishNFTManager.sol b/contracts/interfaces/IStakefishNFTManager.sol new file mode 100644 index 000000000..1c94d499f --- /dev/null +++ b/contracts/interfaces/IStakefishNFTManager.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +interface IStakefishNFTManager { + /// @notice Withdraw from NFT - only allowed for ownerOf(tokenId) + /// @param tokenId of the NFT + function withdraw(uint256 tokenId) external; + + /// @notice computes address based on token + /// @param tokenId of the NFT + /// @return address of the validator contract + function computeAddress(uint256 tokenId) external view returns (address); + + /// @notice lookups the NFT Owner by address => tokenId => owner + /// @param validator address created by mint + /// @return address of the owner + function validatorOwner(address validator) external view returns (address); + + /// @notice lookups the tokenId based on validator address + /// @param validator address created by mint + /// @return tokenId of the NFT + function tokenForValidatorAddr(address validator) + external + view + returns (uint256); + + /// @notice lookups the validator address based on tokenId + /// @param tokenId of the NFT + /// @return address of the validator contract + function validatorForTokenId(uint256 tokenId) + external + view + returns (address); + + /// @notice claim NFT from another NFT Manager, used for migration + /// @param oldManager old nft manager + /// @param tokenId of the NFT on the old manager + function claim(address oldManager, uint256 tokenId) external; + + /// @notice multicall static + function multicallStatic(uint256[] calldata tokenIds, bytes[] calldata data) + external + view + returns (bytes[] memory results); + + /// @notice multicall across multiple tokenIds + function multicall(uint256[] calldata tokenIds, bytes[] calldata data) + external + returns (bytes[] memory results); +} diff --git a/contracts/interfaces/IStakefishValidator.sol b/contracts/interfaces/IStakefishValidator.sol new file mode 100644 index 000000000..2d0b7a7e9 --- /dev/null +++ b/contracts/interfaces/IStakefishValidator.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.10; + +/// @title The interface for StakefishValidator +/// @notice Defines implementation of the wallet (deposit, withdraw, collect fees) +interface IStakefishValidator { + enum State { + PreDeposit, + PostDeposit, + Active, + ExitRequested, + Exited, + Withdrawn, + Burnable + } + + /// @dev aligns into 32 byte + struct StateChange { + State state; // 1 byte + bytes15 userData; // 15 byte (future use) + uint128 changedAt; // 16 byte + } + + function validatorIndex() external view returns (uint256); + + function pubkey() external view returns (bytes memory); + + function withdrawnBalance() external view returns (uint256); + + function feePoolAddress() external view returns (address); + + function stateHistory(uint256 index) + external + view + returns (StateChange memory); + + /// @notice Inspect state of the change + function lastStateChange() external view returns (StateChange memory); + + /// @notice NFT Owner requests a validator exit + /// State.Running -> State.ExitRequested + /// emit ValidatorExitRequest(pubkey) + function requestExit() external; + + /// @notice user withdraw balance and charge a fee + function withdraw() external; + + /// @notice get pending fee pool rewards + function pendingFeePoolReward() external view returns (uint256, uint256); + + /// @notice claim fee pool and forward to nft owner + function claimFeePool(uint256 amountRequested) external; + + function getProtocolFee() external view returns (uint256); + + function getNFTArtUrl() external view returns (string memory); + + /// @notice computes commission, useful for showing on UI + function computeCommission(uint256 amount) external view returns (uint256); + + function render() external view returns (string memory); +} diff --git a/contracts/interfaces/IXTokenType.sol b/contracts/interfaces/IXTokenType.sol index 033339832..dceb3a9fd 100644 --- a/contracts/interfaces/IXTokenType.sol +++ b/contracts/interfaces/IXTokenType.sol @@ -22,7 +22,8 @@ enum XTokenType { NTokenBAKC, PYieldToken, PTokenCAPE, - NTokenOtherdeed + NTokenOtherdeed, + NTokenStakefish } interface IXTokenType { diff --git a/contracts/misc/DepositContract.sol b/contracts/misc/DepositContract.sol new file mode 100644 index 000000000..778966d62 --- /dev/null +++ b/contracts/misc/DepositContract.sol @@ -0,0 +1,227 @@ +// ┏━━━┓━┏┓━┏┓━━┏━━━┓━━┏━━━┓━━━━┏━━━┓━━━━━━━━━━━━━━━━━━━┏┓━━━━━┏━━━┓━━━━━━━━━┏┓━━━━━━━━━━━━━━┏┓━ +// ┃┏━━┛┏┛┗┓┃┃━━┃┏━┓┃━━┃┏━┓┃━━━━┗┓┏┓┃━━━━━━━━━━━━━━━━━━┏┛┗┓━━━━┃┏━┓┃━━━━━━━━┏┛┗┓━━━━━━━━━━━━┏┛┗┓ +// ┃┗━━┓┗┓┏┛┃┗━┓┗┛┏┛┃━━┃┃━┃┃━━━━━┃┃┃┃┏━━┓┏━━┓┏━━┓┏━━┓┏┓┗┓┏┛━━━━┃┃━┗┛┏━━┓┏━┓━┗┓┏┛┏━┓┏━━┓━┏━━┓┗┓┏┛ +// ┃┏━━┛━┃┃━┃┏┓┃┏━┛┏┛━━┃┃━┃┃━━━━━┃┃┃┃┃┏┓┃┃┏┓┃┃┏┓┃┃━━┫┣┫━┃┃━━━━━┃┃━┏┓┃┏┓┃┃┏┓┓━┃┃━┃┏┛┗━┓┃━┃┏━┛━┃┃━ +// ┃┗━━┓━┃┗┓┃┃┃┃┃┃┗━┓┏┓┃┗━┛┃━━━━┏┛┗┛┃┃┃━┫┃┗┛┃┃┗┛┃┣━━┃┃┃━┃┗┓━━━━┃┗━┛┃┃┗┛┃┃┃┃┃━┃┗┓┃┃━┃┗┛┗┓┃┗━┓━┃┗┓ +// ┗━━━┛━┗━┛┗┛┗┛┗━━━┛┗┛┗━━━┛━━━━┗━━━┛┗━━┛┃┏━┛┗━━┛┗━━┛┗┛━┗━┛━━━━┗━━━┛┗━━┛┗┛┗┛━┗━┛┗┛━┗━━━┛┗━━┛━┗━┛ +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┃┃━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┗┛━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.10; + +// This interface is designed to be compatible with the Vyper version. +/// @notice This is the Ethereum 2.0 deposit contract interface. +/// For more information see the Phase 0 specification under https://github.com/ethereum/eth2.0-specs +interface IDepositContract { + /// @notice A processed deposit event. + event DepositEvent( + bytes pubkey, + bytes withdrawal_credentials, + bytes amount, + bytes signature, + bytes index + ); + + /// @notice Submit a Phase 0 DepositData object. + /// @param pubkey A BLS12-381 public key. + /// @param withdrawal_credentials Commitment to a public key for withdrawals. + /// @param signature A BLS12-381 signature. + /// @param deposit_data_root The SHA-256 hash of the SSZ-encoded DepositData object. + /// Used as a protection against malformed input. + function deposit( + bytes calldata pubkey, + bytes calldata withdrawal_credentials, + bytes calldata signature, + bytes32 deposit_data_root + ) external payable; + + /// @notice Query the current deposit root hash. + /// @return The deposit root hash. + function get_deposit_root() external view returns (bytes32); + + /// @notice Query the current deposit count. + /// @return The deposit count encoded as a little endian 64-bit number. + function get_deposit_count() external view returns (bytes memory); +} + +// Based on official specification in https://eips.ethereum.org/EIPS/eip-165 +interface ERC165 { + /// @notice Query if a contract implements an interface + /// @param interfaceId The interface identifier, as specified in ERC-165 + /// @dev Interface identification is specified in ERC-165. This function + /// uses less than 30,000 gas. + /// @return `true` if the contract implements `interfaceId` and + /// `interfaceId` is not 0xffffffff, `false` otherwise + function supportsInterface(bytes4 interfaceId) external pure returns (bool); +} + +// This is a rewrite of the Vyper Eth2.0 deposit contract in Solidity. +// It tries to stay as close as possible to the original source code. +/// @notice This is the Ethereum 2.0 deposit contract interface. +/// For more information see the Phase 0 specification under https://github.com/ethereum/eth2.0-specs +contract DepositContract is IDepositContract, ERC165 { + uint256 constant DEPOSIT_CONTRACT_TREE_DEPTH = 32; + // NOTE: this also ensures `deposit_count` will fit into 64-bits + uint256 constant MAX_DEPOSIT_COUNT = 2**DEPOSIT_CONTRACT_TREE_DEPTH - 1; + + bytes32[DEPOSIT_CONTRACT_TREE_DEPTH] branch; + uint256 deposit_count; + + bytes32[DEPOSIT_CONTRACT_TREE_DEPTH] zero_hashes; + + constructor() public { + // Compute hashes in empty sparse Merkle tree + for ( + uint256 height = 0; + height < DEPOSIT_CONTRACT_TREE_DEPTH - 1; + height++ + ) + zero_hashes[height + 1] = sha256( + abi.encodePacked(zero_hashes[height], zero_hashes[height]) + ); + } + + function get_deposit_root() external view override returns (bytes32) { + bytes32 node; + uint256 size = deposit_count; + for ( + uint256 height = 0; + height < DEPOSIT_CONTRACT_TREE_DEPTH; + height++ + ) { + if ((size & 1) == 1) + node = sha256(abi.encodePacked(branch[height], node)); + else node = sha256(abi.encodePacked(node, zero_hashes[height])); + size /= 2; + } + return + sha256( + abi.encodePacked( + node, + to_little_endian_64(uint64(deposit_count)), + bytes24(0) + ) + ); + } + + function get_deposit_count() external view override returns (bytes memory) { + return to_little_endian_64(uint64(deposit_count)); + } + + function deposit( + bytes calldata pubkey, + bytes calldata withdrawal_credentials, + bytes calldata signature, + bytes32 + ) external payable override { + // Extended ABI length checks since dynamic types are used. + require(pubkey.length == 48, "DepositContract: invalid pubkey length"); + require( + withdrawal_credentials.length == 32, + "DepositContract: invalid withdrawal_credentials length" + ); + require( + signature.length == 96, + "DepositContract: invalid signature length" + ); + + // Check deposit amount + require(msg.value >= 1 ether, "DepositContract: deposit value too low"); + require( + msg.value % 1 gwei == 0, + "DepositContract: deposit value not multiple of gwei" + ); + uint256 deposit_amount = msg.value / 1 gwei; + require( + deposit_amount <= type(uint64).max, + "DepositContract: deposit value too high" + ); + + // Emit `DepositEvent` log + bytes memory amount = to_little_endian_64(uint64(deposit_amount)); + emit DepositEvent( + pubkey, + withdrawal_credentials, + amount, + signature, + to_little_endian_64(uint64(deposit_count)) + ); + + // Compute deposit data root (`DepositData` hash tree root) + bytes32 pubkey_root = sha256(abi.encodePacked(pubkey, bytes16(0))); + bytes32 signature_root = sha256( + abi.encodePacked( + sha256(abi.encodePacked(signature[:64])), + sha256(abi.encodePacked(signature[64:], bytes32(0))) + ) + ); + bytes32 node = sha256( + abi.encodePacked( + sha256(abi.encodePacked(pubkey_root, withdrawal_credentials)), + sha256(abi.encodePacked(amount, bytes24(0), signature_root)) + ) + ); + + // Verify computed and expected deposit data roots match + // require( + // node == deposit_data_root, + // "DepositContract: reconstructed DepositData does not match supplied deposit_data_root" + // ); + + // Avoid overflowing the Merkle tree (and prevent edge case in computing `branch`) + require( + deposit_count < MAX_DEPOSIT_COUNT, + "DepositContract: merkle tree full" + ); + + // Add deposit data root to Merkle tree (update a single `branch` node) + deposit_count += 1; + uint256 size = deposit_count; + for ( + uint256 height = 0; + height < DEPOSIT_CONTRACT_TREE_DEPTH; + height++ + ) { + if ((size & 1) == 1) { + branch[height] = node; + return; + } + node = sha256(abi.encodePacked(branch[height], node)); + size /= 2; + } + // As the loop should always end prematurely with the `return` statement, + // this code should be unreachable. We assert `false` just to be safe. + assert(false); + } + + function supportsInterface(bytes4 interfaceId) + external + pure + override + returns (bool) + { + return + interfaceId == type(ERC165).interfaceId || + interfaceId == type(IDepositContract).interfaceId; + } + + function to_little_endian_64(uint64 value) + internal + pure + returns (bytes memory ret) + { + ret = new bytes(8); + bytes8 bytesValue = bytes8(value); + // Byteswapping during copying to bytes. + ret[0] = bytesValue[7]; + ret[1] = bytesValue[6]; + ret[2] = bytesValue[5]; + ret[3] = bytesValue[4]; + ret[4] = bytesValue[3]; + ret[5] = bytesValue[2]; + ret[6] = bytesValue[1]; + ret[7] = bytesValue[0]; + } +} diff --git a/contracts/misc/ParaSpaceOracle.sol b/contracts/misc/ParaSpaceOracle.sol index 1111969f3..cdef1fac5 100644 --- a/contracts/misc/ParaSpaceOracle.sol +++ b/contracts/misc/ParaSpaceOracle.sol @@ -152,40 +152,6 @@ contract ParaSpaceOracle is IParaSpaceOracle { revert(Errors.ORACLE_PRICE_NOT_READY); } - function getTokensPrices(address asset, uint256[] calldata tokenIds) - external - view - override - returns (uint256[] memory) - { - IAtomicPriceAggregator source = IAtomicPriceAggregator( - assetsSources[asset] - ); - - if (address(source) != address(0)) { - return source.getTokensPrices(tokenIds); - } - - revert(Errors.ORACLE_PRICE_NOT_READY); - } - - function getTokensPricesSum(address asset, uint256[] calldata tokenIds) - external - view - override - returns (uint256) - { - IAtomicPriceAggregator source = IAtomicPriceAggregator( - assetsSources[asset] - ); - - if (address(source) != address(0)) { - return source.getTokensPricesSum(tokenIds); - } - - revert(Errors.ORACLE_PRICE_NOT_READY); - } - /// @inheritdoc IParaSpaceOracle function getAssetsPrices(address[] calldata assets) external diff --git a/contracts/misc/StakefishNFTOracleWrapper.sol b/contracts/misc/StakefishNFTOracleWrapper.sol new file mode 100644 index 000000000..c99568038 --- /dev/null +++ b/contracts/misc/StakefishNFTOracleWrapper.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.10; + +import {IStakefishNFTManager} from "../interfaces/IStakefishNFTManager.sol"; +import {IStakefishValidator} from "../interfaces/IStakefishValidator.sol"; +import {SafeCast} from "../dependencies/univ3/libraries/SafeCast.sol"; +import {IAtomicPriceAggregator} from "../interfaces/IAtomicPriceAggregator.sol"; + +contract StakefishNFTOracleWrapper is IAtomicPriceAggregator { + using SafeCast for uint256; + + IStakefishNFTManager immutable STAKEFISH_NFT_MANAGER; + + // 28 ether is used to take slashes & penalties into account + // this is important since both withdrawnBalance, availableBalance and deposited 32 ether + // are quite dynamic & can be manipulated. + // + // the original 32 ether is either in: + // - withdrawnBalance + // - availableBalance + // - deposit contract + // we use 28 ether to detect where it is + uint256 public constant MIN_FULL_WITHDRAWAL = 28 ether; + + constructor(address _stakefishNFTManager) { + STAKEFISH_NFT_MANAGER = IStakefishNFTManager(_stakefishNFTManager); + } + + function getTokenPrice(uint256 tokenId) public view returns (uint256) { + address validatorAddr = STAKEFISH_NFT_MANAGER.validatorForTokenId( + tokenId + ); + IStakefishValidator.StateChange memory lastState = IStakefishValidator( + validatorAddr + ).lastStateChange(); + uint256 availableBalance = address(validatorAddr).balance; + uint256 withdrawnBalance = IStakefishValidator(validatorAddr) + .withdrawnBalance(); + + // funds are not deposited into deposit contract yet + if (lastState.state < IStakefishValidator.State.PostDeposit) { + return availableBalance; + } + + if (lastState.state < IStakefishValidator.State.Withdrawn) { + // 1. already withdrawn + if (withdrawnBalance >= MIN_FULL_WITHDRAWAL) { + uint256 commission = (availableBalance * + IStakefishValidator(validatorAddr).getProtocolFee()) / + 10000; + + return availableBalance - commission; + } + + // 2. full withdrawal funds arrived via system operations + if (availableBalance >= MIN_FULL_WITHDRAWAL) { + if (withdrawnBalance + availableBalance <= 32 ether) { + return availableBalance; + } else { + uint256 commissionApplyBalance = availableBalance + + withdrawnBalance - + 32 ether; + uint256 commission = (commissionApplyBalance * + IStakefishValidator(validatorAddr).getProtocolFee()) / + 10000; + return availableBalance - commission; + } + } + + // 3. funds are still in deposit contract and validator didn't exit + return 32 ether + availableBalance; + } + + return 0; + } +} diff --git a/contracts/misc/UniswapV3OracleWrapper.sol b/contracts/misc/UniswapV3OracleWrapper.sol index 5d2c0ccc2..03080a36d 100644 --- a/contracts/misc/UniswapV3OracleWrapper.sol +++ b/contracts/misc/UniswapV3OracleWrapper.sol @@ -184,44 +184,6 @@ contract UniswapV3OracleWrapper is IUniswapV3OracleWrapper { 10**oracleData.token1Decimal); } - /** - * @notice Returns the price for the specified tokenId array. - */ - function getTokensPrices(uint256[] calldata tokenIds) - external - view - returns (uint256[] memory) - { - uint256[] memory prices = new uint256[](tokenIds.length); - - for (uint256 index = 0; index < tokenIds.length; index++) { - prices[index] = getTokenPrice(tokenIds[index]); - } - - return prices; - } - - /** - * @notice Returns the total price for the specified tokenId array. - */ - function getTokensPricesSum(uint256[] calldata tokenIds) - external - view - returns (uint256) - { - uint256 sum = 0; - - for (uint256 index = 0; index < tokenIds.length; index++) { - sum += getTokenPrice(tokenIds[index]); - } - - return sum; - } - - function latestAnswer() external pure returns (int256) { - revert("unimplemented"); - } - function _getOracleData(UinswapV3PositionData memory positionData) internal view diff --git a/contracts/protocol/libraries/helpers/Errors.sol b/contracts/protocol/libraries/helpers/Errors.sol index 35267304c..ea9cd8fca 100644 --- a/contracts/protocol/libraries/helpers/Errors.sol +++ b/contracts/protocol/libraries/helpers/Errors.sol @@ -111,12 +111,12 @@ library Errors { string public constant AUCTIONED_BALANCE_NOT_ZERO = "116"; //auctioned balance not zero. string public constant LIQUIDATOR_CAN_NOT_BE_SELF = "117"; //user can not liquidate himself. string public constant INVALID_RECIPIENT = "118"; //invalid recipient specified in order. - string public constant UNIV3_NOT_ALLOWED = "119"; //flash claim is not allowed for UniswapV3. + string public constant FLASHCLAIM_NOT_ALLOWED = "119"; //flash claim is not allowed for UniswapV3. string public constant NTOKEN_BALANCE_EXCEEDED = "120"; //ntoken balance exceed limit. string public constant ORACLE_PRICE_NOT_READY = "121"; //oracle price not ready. string public constant SET_ORACLE_SOURCE_NOT_ALLOWED = "122"; //source of oracle not allowed to set. string public constant INVALID_LIQUIDATION_ASSET = "123"; //invalid liquidation asset. - string public constant ONLY_UNIV3_ALLOWED = "124"; //only UniswapV3 allowed. + string public constant XTOKEN_TYPE_NOT_ALLOWED = "124"; //only UniswapV3 allowed. string public constant GLOBAL_DEBT_IS_ZERO = "125"; //liquidation is not allowed when global debt is zero. string public constant ORACLE_PRICE_EXPIRED = "126"; //oracle price expired. string public constant APE_STAKING_POSITION_EXISTED = "127"; //ape staking position is existed. diff --git a/contracts/protocol/libraries/helpers/Helpers.sol b/contracts/protocol/libraries/helpers/Helpers.sol index c7cf3fc81..5c7acaa39 100644 --- a/contracts/protocol/libraries/helpers/Helpers.sol +++ b/contracts/protocol/libraries/helpers/Helpers.sol @@ -36,4 +36,14 @@ library Helpers { .getTraitMultiplier(tokenId); return assetPrice.wadMul(multiplier); } + + /** + * @dev transfer ETH to an address, revert if it fails. + * @param to recipient of the transfer + * @param value the amount to send + */ + function safeTransferETH(address to, uint256 value) internal { + (bool success, ) = to.call{value: value}(new bytes(0)); + require(success, "ETH_TRANSFER_FAILED"); + } } diff --git a/contracts/protocol/libraries/logic/GenericLogic.sol b/contracts/protocol/libraries/logic/GenericLogic.sol index 08cb9e617..430064baa 100644 --- a/contracts/protocol/libraries/logic/GenericLogic.sol +++ b/contracts/protocol/libraries/logic/GenericLogic.sol @@ -366,18 +366,44 @@ library GenericLogic { DataTypes.CalculateUserAccountDataParams memory params, CalculateUserAccountDataVars memory vars ) private view returns (uint256 totalValue) { - uint256 assetPrice = _getAssetPrice( - params.oracle, - vars.currentReserveAddress - ); + INToken nToken = INToken(vars.xTokenAddress); + bool isAtomicPrice = IAtomicCollateralizableERC721(vars.xTokenAddress) + .isAtomicPricing(); + if (isAtomicPrice) { + uint256 totalBalance = nToken.balanceOf(params.user); + + for (uint256 index = 0; index < totalBalance; index++) { + uint256 tokenId = nToken.tokenOfOwnerByIndex( + params.user, + index + ); + if ( + ICollateralizableERC721(vars.xTokenAddress) + .isUsedAsCollateral(tokenId) + ) { + totalValue += _getTokenPrice( + params.oracle, + vars.currentReserveAddress, + tokenId + ); + } + } + } else { + uint256 assetPrice = _getAssetPrice( + params.oracle, + vars.currentReserveAddress + ); - uint256 collateralizedBalance = ICollateralizableERC721( - vars.xTokenAddress - ).collateralizedBalanceOf(params.user); - uint256 avgMultiplier = IAtomicCollateralizableERC721( - vars.xTokenAddress - ).avgMultiplierOf(params.user); - totalValue = (collateralizedBalance * avgMultiplier).wadMul(assetPrice); + uint256 collateralizedBalance = ICollateralizableERC721( + vars.xTokenAddress + ).collateralizedBalanceOf(params.user); + uint256 avgMultiplier = IAtomicCollateralizableERC721( + vars.xTokenAddress + ).avgMultiplierOf(params.user); + totalValue = (collateralizedBalance * avgMultiplier).wadMul( + assetPrice + ); + } } function getLtvAndLTForUniswapV3( diff --git a/contracts/protocol/libraries/logic/MarketplaceLogic.sol b/contracts/protocol/libraries/logic/MarketplaceLogic.sol index 0c6c628dc..041b7c346 100644 --- a/contracts/protocol/libraries/logic/MarketplaceLogic.sol +++ b/contracts/protocol/libraries/logic/MarketplaceLogic.sol @@ -546,7 +546,7 @@ library MarketplaceLogic { require( INToken(vars.xTokenAddress).getXTokenType() != XTokenType.NTokenUniswapV3, - Errors.UNIV3_NOT_ALLOWED + Errors.XTOKEN_TYPE_NOT_ALLOWED ); // item.token == underlyingAsset but supplied after listing/offering diff --git a/contracts/protocol/libraries/logic/SupplyLogic.sol b/contracts/protocol/libraries/logic/SupplyLogic.sol index 7d8a4bda2..7e4ec8ef6 100644 --- a/contracts/protocol/libraries/logic/SupplyLogic.sol +++ b/contracts/protocol/libraries/logic/SupplyLogic.sol @@ -20,7 +20,10 @@ import {ValidationLogic} from "./ValidationLogic.sol"; import {ReserveLogic} from "./ReserveLogic.sol"; import {XTokenType} from "../../../interfaces/IXTokenType.sol"; import {INTokenUniswapV3} from "../../../interfaces/INTokenUniswapV3.sol"; +import {INTokenStakefish} from "../../../interfaces/INTokenStakefish.sol"; import {GenericLogic} from "./GenericLogic.sol"; +import {IStakefishNFTManager} from "../../../interfaces/IStakefishNFTManager.sol"; +import {IStakefishValidator} from "../../../interfaces/IStakefishValidator.sol"; /** * @title SupplyLogic library @@ -201,6 +204,17 @@ library SupplyLogic { ); } } + if (tokenType == XTokenType.NTokenStakefish) { + for (uint256 index = 0; index < params.tokenData.length; index++) { + address validatorAddr = IStakefishNFTManager(params.asset) + .validatorForTokenId(params.tokenData[index].tokenId); + IStakefishValidator.StateChange + memory lastState = IStakefishValidator(validatorAddr) + .lastStateChange(); + // TODO: add error code + require(lastState.state < IStakefishValidator.State.Withdrawn); + } + } if ( tokenType == XTokenType.NTokenBAYC || tokenType == XTokenType.NTokenMAYC @@ -449,11 +463,11 @@ library SupplyLogic { ); } - function executeDecreaseUniswapV3Liquidity( + function executeClaimStakefishWithdrawals( mapping(address => DataTypes.ReserveData) storage reservesData, mapping(uint256 => address) storage reservesList, DataTypes.UserConfigurationMap storage userConfig, - DataTypes.ExecuteDecreaseUniswapV3LiquidityParams memory params + DataTypes.ExecuteClaimStakefishWithdrawalsParams memory params ) external { DataTypes.ReserveData storage reserve = reservesData[params.asset]; DataTypes.ReserveCache memory reserveCache = reserve.cache(); @@ -461,10 +475,54 @@ library SupplyLogic { //currently don't need to update state for erc721 //reserve.updateState(reserveCache); + INToken nToken = INToken(reserveCache.xTokenAddress); + require( + nToken.getXTokenType() == XTokenType.NTokenStakefish, + Errors.XTOKEN_TYPE_NOT_ALLOWED + ); + + ValidationLogic.validateWithdrawERC721( + reservesData, + reserveCache, + params.asset, + params.tokenIds + ); + + INTokenStakefish(reserveCache.xTokenAddress).withdraw( + msg.sender, + params.tokenIds, + params.to + ); + + if (userConfig.isBorrowingAny()) { + ValidationLogic.validateHFAndLtvERC721( + reservesData, + reservesList, + userConfig, + params.asset, + params.tokenIds, + msg.sender, + params.reservesCount, + params.oracle + ); + } + } + + function executeDecreaseUniswapV3Liquidity( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.ExecuteDecreaseUniswapV3LiquidityParams memory params + ) external { + DataTypes.ReserveData storage reserve = reservesData[params.asset]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + + //currently don't need to update state for erc721 + //reserve.uONLY_UNIV3_ALLOWEDCache); INToken nToken = INToken(reserveCache.xTokenAddress); require( nToken.getXTokenType() == XTokenType.NTokenUniswapV3, - Errors.ONLY_UNIV3_ALLOWED + Errors.XTOKEN_TYPE_NOT_ALLOWED ); uint256[] memory tokenIds = new uint256[](1); diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 18dde7465..42086bde3 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -1011,8 +1011,9 @@ library ValidationLogic { INToken nToken = INToken(xTokenAddress); XTokenType tokenType = nToken.getXTokenType(); require( - tokenType != XTokenType.NTokenUniswapV3, - Errors.UNIV3_NOT_ALLOWED + tokenType != XTokenType.NTokenUniswapV3 && + tokenType != XTokenType.NTokenStakefish, + Errors.XTOKEN_TYPE_NOT_ALLOWED ); // need check sApe status when flash claim for bayc or mayc diff --git a/contracts/protocol/libraries/types/DataTypes.sol b/contracts/protocol/libraries/types/DataTypes.sol index 48d192fa2..8168373a1 100644 --- a/contracts/protocol/libraries/types/DataTypes.sol +++ b/contracts/protocol/libraries/types/DataTypes.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.10; import {OfferItem, ConsiderationItem} from "../../../dependencies/seaport/contracts/lib/ConsiderationStructs.sol"; +import {IStakefishValidator} from "../../../interfaces/IStakefishValidator.sol"; library DataTypes { enum AssetType { @@ -81,11 +82,23 @@ library DataTypes { bool useAsCollateral; } + struct StakefishNTokenData { + uint256 validatorIndex; + bytes pubkey; + uint256 withdrawnBalance; + address feePoolAddress; + string nftArtUrl; + uint256 protocolFee; + IStakefishValidator.StateChange[] stateHistory; + uint256[2] pendingFeePoolReward; + } + struct NTokenData { uint256 tokenId; uint256 multiplier; bool useAsCollateral; bool isAuctioned; + StakefishNTokenData stakefishNTokenData; } struct ReserveCache { @@ -192,6 +205,14 @@ library DataTypes { address oracle; } + struct ExecuteClaimStakefishWithdrawalsParams { + address asset; + uint256[] tokenIds; + address to; + uint256 reservesCount; + address oracle; + } + struct FinalizeTransferParams { address asset; address from; diff --git a/contracts/protocol/pool/PoolCore.sol b/contracts/protocol/pool/PoolCore.sol index 3bc1b9904..751e98199 100644 --- a/contracts/protocol/pool/PoolCore.sol +++ b/contracts/protocol/pool/PoolCore.sol @@ -237,6 +237,29 @@ contract PoolCore is ); } + /// @inheritdoc IPoolCore + function claimStakefishWithdrawals( + address asset, + uint256[] calldata tokenIds, + address to + ) external nonReentrant { + DataTypes.PoolStorage storage ps = poolStorage(); + + return + SupplyLogic.executeClaimStakefishWithdrawals( + ps._reserves, + ps._reservesList, + ps._usersConfig[msg.sender], + DataTypes.ExecuteClaimStakefishWithdrawalsParams({ + asset: asset, + tokenIds: tokenIds, + to: to, + reservesCount: ps._reservesCount, + oracle: ADDRESSES_PROVIDER.getPriceOracle() + }) + ); + } + function decreaseUniswapV3Liquidity( address asset, uint256 tokenId, diff --git a/contracts/protocol/tokenization/NTokenStakefish.sol b/contracts/protocol/tokenization/NTokenStakefish.sol new file mode 100644 index 000000000..6bc3e653d --- /dev/null +++ b/contracts/protocol/tokenization/NTokenStakefish.sol @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.10; + +import {IStakefishNFTManager} from "../../interfaces/IStakefishNFTManager.sol"; +import {IStakefishValidator} from "../../interfaces/IStakefishValidator.sol"; +import {INTokenStakefish} from "../../interfaces/INTokenStakefish.sol"; +import {NToken} from "./NToken.sol"; +import {IPool} from "../../interfaces/IPool.sol"; +import {XTokenType} from "../../interfaces/IXTokenType.sol"; +import {Errors} from "../libraries/helpers/Errors.sol"; +import {Helpers} from "../libraries/helpers/Helpers.sol"; +import {DataTypes} from "../libraries/types/DataTypes.sol"; + +/** + * @title NTokenStakefish + * + * @notice Implementation of the NFT derivative token for the ParaSpace protocol + */ +contract NTokenStakefish is NToken, INTokenStakefish { + /** + * @dev Constructor. + * @param pool The address of the Pool contract + */ + constructor(IPool pool, address delegateRegistry) + NToken(pool, true, delegateRegistry) + {} + + function getXTokenType() external pure override returns (XTokenType) { + return XTokenType.NTokenStakefish; + } + + // @inheritdoc INTokenStakefish + function withdraw( + address user, + uint256[] calldata tokenIds, + address to + ) external onlyPool nonReentrant { + uint256 beforeBalance = address(this).balance; + for (uint256 index = 0; index < tokenIds.length; index++) { + require( + user == _ERC721Data.owners[tokenIds[index]], + Errors.NOT_THE_OWNER + ); + address validatorAddr = _getValidatorAddr(tokenIds[index]); + IStakefishValidator(validatorAddr).withdraw(); + } + uint256 diff = address(this).balance - beforeBalance; + if (diff > 0) Helpers.safeTransferETH(to, diff); + } + + // @inheritdoc INTokenStakefish + function claimFeePool( + uint256[] calldata tokenIds, + uint256[] calldata amountsRequested, + address to + ) external nonReentrant { + require( + tokenIds.length == amountsRequested.length, + Errors.INCONSISTENT_PARAMS_LENGTH + ); + uint256 beforeBalance = address(this).balance; + for (uint256 index = 0; index < tokenIds.length; index++) { + require( + msg.sender == _ERC721Data.owners[tokenIds[index]], + Errors.NOT_THE_OWNER + ); + address validatorAddr = _getValidatorAddr(tokenIds[index]); + IStakefishValidator(validatorAddr).claimFeePool( + amountsRequested[index] + ); + } + uint256 diff = address(this).balance - beforeBalance; + if (diff > 0) Helpers.safeTransferETH(to, diff); + } + + // @inheritdoc INTokenStakefish + function requestExit(uint256[] calldata tokenIds) external nonReentrant { + for (uint256 index = 0; index < tokenIds.length; index++) { + require( + msg.sender == _ERC721Data.owners[tokenIds[index]], + Errors.NOT_THE_OWNER + ); + address validatorAddr = _getValidatorAddr(tokenIds[index]); + IStakefishValidator(validatorAddr).requestExit(); + } + } + + // @inheritdoc INTokenStakefish + function getNFTData(uint256 tokenId) + external + view + returns (DataTypes.StakefishNTokenData memory data) + { + address validatorAddr = _getValidatorAddr(tokenId); + data.validatorIndex = IStakefishValidator(validatorAddr) + .validatorIndex(); + data.pubkey = IStakefishValidator(validatorAddr).pubkey(); + data.withdrawnBalance = IStakefishValidator(validatorAddr) + .withdrawnBalance(); + data.feePoolAddress = IStakefishValidator(validatorAddr) + .feePoolAddress(); + data.nftArtUrl = IStakefishValidator(validatorAddr).getNFTArtUrl(); + data.protocolFee = IStakefishValidator(validatorAddr).getProtocolFee(); + IStakefishValidator.StateChange + memory lastStateChange = IStakefishValidator(validatorAddr) + .lastStateChange(); + + uint8 size = uint8(lastStateChange.state) + 1; + IStakefishValidator.StateChange[] + memory stateHistory = new IStakefishValidator.StateChange[](size); + for (uint256 i = 0; i < size; i++) { + stateHistory[i] = IStakefishValidator(validatorAddr).stateHistory( + i + ); + } + data.stateHistory = stateHistory; + + if (data.feePoolAddress != address(0)) { + ( + uint256 pendingRewards, + uint256 collectedRewards + ) = IStakefishValidator(validatorAddr).pendingFeePoolReward(); + data.pendingFeePoolReward[0] = pendingRewards; + data.pendingFeePoolReward[1] = collectedRewards; + } + } + + // Internal function to get the validator contract address for the given tokenId + function _getValidatorAddr(uint256 tokenId) + internal + view + returns (address) + { + // Get the validator address from the underlying Stakefish NFTManager contract + address validatorAddr = IStakefishNFTManager( + _ERC721Data.underlyingAsset + ).validatorForTokenId(tokenId); + // Ensure that the validator address is not zero + require(validatorAddr != address(0), Errors.ZERO_ADDRESS_NOT_VALID); + return validatorAddr; + } + + function setTraitsMultipliers(uint256[] calldata, uint256[] calldata) + external + override + onlyPoolAdmin + nonReentrant + { + revert(); + } + + receive() external payable {} +} diff --git a/contracts/ui/UiPoolDataProvider.sol b/contracts/ui/UiPoolDataProvider.sol index bcbc75173..edec57fc1 100644 --- a/contracts/ui/UiPoolDataProvider.sol +++ b/contracts/ui/UiPoolDataProvider.sol @@ -30,6 +30,8 @@ import {DataTypes} from "../protocol/libraries/types/DataTypes.sol"; import {IUniswapV3OracleWrapper} from "../interfaces/IUniswapV3OracleWrapper.sol"; import {UinswapV3PositionData} from "../interfaces/IUniswapV3PositionInfoProvider.sol"; import {Helpers} from "../protocol/libraries/helpers/Helpers.sol"; +import {IStakefishValidator} from "../interfaces/IStakefishValidator.sol"; +import {INTokenStakefish} from "../interfaces/INTokenStakefish.sol"; contract UiPoolDataProvider is IUiPoolDataProvider { using WadRayMath for uint256; @@ -299,6 +301,7 @@ contract UiPoolDataProvider is IUiPoolDataProvider { address asset = nTokenAddresses[i]; uint256 size = tokenIds[i].length; tokenData[i] = new DataTypes.NTokenData[](size); + XTokenType xTokenType = IXTokenType(asset).getXTokenType(); for (uint256 j = 0; j < size; j++) { tokenData[i][j].tokenId = tokenIds[i][j]; @@ -309,6 +312,12 @@ contract UiPoolDataProvider is IUiPoolDataProvider { tokenData[i][j].multiplier = IAtomicCollateralizableERC721( asset ).getTraitMultiplier(tokenIds[i][j]); + + if (xTokenType == XTokenType.NTokenStakefish) { + tokenData[i][j].stakefishNTokenData = INTokenStakefish( + asset + ).getNFTData(tokenIds[i][j]); + } } } diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index fd8cd7840..12ae86ae7 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -269,8 +269,20 @@ import { NTokenOtherdeed, HotWalletProxy__factory, HotWalletProxy, + NTokenStakefish__factory, + NTokenStakefish, + StakefishNFTOracleWrapper__factory, + StakefishNFTOracleWrapper, DelegationRegistry, DelegationRegistry__factory, + StakefishNFTManager__factory, + StakefishNFTManager, + StakefishValidatorV1__factory, + StakefishValidatorV1, + DepositContract__factory, + DepositContract, + StakefishValidatorFactory__factory, + StakefishValidatorFactory, } from "../types"; import {MockContract} from "ethereum-waffle"; import { @@ -291,6 +303,7 @@ import { getContractAddressInDb, getFunctionSignatures, getFunctionSignaturesFromDb, + getParaSpaceAdmins, insertContractAddressInDb, withSaveAndVerify, } from "./contracts-helpers"; @@ -1201,7 +1214,8 @@ export const deployAllERC721Tokens = async (verify?: boolean) => { | Land | Meebits | Moonbirds - | Contract; + | Contract + | StakefishNFTManager; } = {}; const paraSpaceConfig = getParaSpaceConfig(); const reservesConfig = paraSpaceConfig.ReservesConfig; @@ -1260,6 +1274,16 @@ export const deployAllERC721Tokens = async (verify?: boolean) => { false ); } + if ( + tokenSymbol === ERC721TokenContractId.SFVLDR && + paraSpaceConfig.StakefishManager + ) { + await insertContractAddressInDb( + eContractid.SFVLDR, + paraSpaceConfig.StakefishManager, + false + ); + } continue; } else { console.log("deploying now ", tokenSymbol); @@ -1379,6 +1403,30 @@ export const deployAllERC721Tokens = async (verify?: boolean) => { continue; } + if (tokenSymbol === ERC721TokenContractId.SFVLDR) { + const depositContract = await deployDepositContract(verify); + const {paraSpaceAdminAddress} = await getParaSpaceAdmins(); + const validatorImpl = await deployStakefishValidator( + depositContract.address, + verify + ); + + const factory = await deployStakefishValidatorFactory( + validatorImpl.address, + paraSpaceAdminAddress, + verify + ); + + const nftManager = await deployStakefishNFTManager( + factory.address, + verify + ); + await waitForTx(await factory.setDeployer(nftManager.address, true)); + + tokens[tokenSymbol] = nftManager; + continue; + } + tokens[tokenSymbol] = await deployMintableERC721( [tokenSymbol, tokenSymbol, ""], verify @@ -1930,6 +1978,17 @@ export const deployUniswapV3TwapOracleWrapper = async ( verify ) as Promise; +export const deployStakefishNFTOracleWrapper = async ( + stakefishManager: tEthereumAddress, + verify?: boolean +) => + withSaveAndVerify( + new StakefishNFTOracleWrapper__factory(await getFirstSigner()), + eContractid.Aggregator.concat(upperFirst(eContractid.SFVLDR)), + [stakefishManager], + verify + ) as Promise; + export const deployNonfungiblePositionManager = async ( args: [string, string, string], verify?: boolean @@ -2823,6 +2882,111 @@ export const deployReserveTimeLockStrategy = async ( verify ) as Promise; +export const deployOtherdeedNTokenImpl = async ( + poolAddress: tEthereumAddress, + warmWallet: tEthereumAddress, + delegationRegistryAddress: tEthereumAddress, + verify?: boolean +) => { + const mintableERC721Logic = + (await getContractAddressInDb(eContractid.MintableERC721Logic)) || + (await deployMintableERC721Logic(verify)).address; + + const libraries = { + ["contracts/protocol/tokenization/libraries/MintableERC721Logic.sol:MintableERC721Logic"]: + mintableERC721Logic, + }; + return withSaveAndVerify( + new NTokenOtherdeed__factory(libraries, await getFirstSigner()), + eContractid.NTokenOtherdeedImpl, + [poolAddress, warmWallet, delegationRegistryAddress], + verify, + false, + libraries + ) as Promise; +}; + +export const deployStakefishNTokenImpl = async ( + poolAddress: tEthereumAddress, + delegationRegistryAddress: tEthereumAddress, + verify?: boolean +) => { + const mintableERC721Logic = + (await getContractAddressInDb(eContractid.MintableERC721Logic)) || + (await deployMintableERC721Logic(verify)).address; + + const libraries = { + ["contracts/protocol/tokenization/libraries/MintableERC721Logic.sol:MintableERC721Logic"]: + mintableERC721Logic, + }; + return withSaveAndVerify( + new NTokenStakefish__factory(libraries, await getFirstSigner()), + eContractid.NTokenStakefishImpl, + [poolAddress, delegationRegistryAddress], + verify, + false, + libraries + ) as Promise; +}; + +export const deployHotWalletProxy = async (verify?: boolean) => + withSaveAndVerify( + new HotWalletProxy__factory(await getFirstSigner()), + eContractid.HotWalletProxy, + [], + verify + ) as Promise; + +export const deployDelegationRegistry = async (verify?: boolean) => + withSaveAndVerify( + new DelegationRegistry__factory(await getFirstSigner()), + eContractid.DelegationRegistry, + [], + verify + ) as Promise; + +export const deployStakefishValidatorFactory = async ( + genesisImplementation: tEthereumAddress, + operator: tEthereumAddress, + verify?: boolean +) => + withSaveAndVerify( + new StakefishValidatorFactory__factory(await getFirstSigner()), + eContractid.StakefishValidatorFactory, + [genesisImplementation, operator], + verify + ) as Promise; + +export const deployStakefishNFTManager = async ( + factory: tEthereumAddress, + verify?: boolean +) => + withSaveAndVerify( + new StakefishNFTManager__factory(await getFirstSigner()), + eContractid.SFVLDR, + [factory], + verify + ) as Promise; + +export const deployDepositContract = async (verify?: boolean) => + withSaveAndVerify( + new DepositContract__factory(await getFirstSigner()), + eContractid.DepositContract, + [], + verify + ) as Promise; + +export const deployStakefishValidator = async ( + depositContract: tEthereumAddress, + verify?: boolean +) => + withSaveAndVerify( + new StakefishValidatorV1__factory(await getFirstSigner()), + eContractid.StakefishValidator, + [depositContract], + verify + ) as Promise; + //////////////////////////////////////////////////////////////////////////////// // MOCK //////////////////////////////////////////////////////////////////////////////// @@ -3081,43 +3245,6 @@ export const deployMockedDelegateRegistry = async (verify?: boolean) => [], verify ) as Promise; - -export const deployOtherdeedNTokenImpl = async ( - poolAddress: tEthereumAddress, - warmWallet: tEthereumAddress, - delegationRegistryAddress: tEthereumAddress, - verify?: boolean -) => { - const mintableERC721Logic = - (await getContractAddressInDb(eContractid.MintableERC721Logic)) || - (await deployMintableERC721Logic(verify)).address; - - const libraries = { - ["contracts/protocol/tokenization/libraries/MintableERC721Logic.sol:MintableERC721Logic"]: - mintableERC721Logic, - }; - return withSaveAndVerify( - new NTokenOtherdeed__factory(libraries, await getFirstSigner()), - eContractid.NTokenOtherdeedImpl, - [poolAddress, warmWallet, delegationRegistryAddress], - verify, - false, - libraries - ) as Promise; -}; - -export const deployHotWalletProxy = async (verify?: boolean) => - withSaveAndVerify( - new HotWalletProxy__factory(await getFirstSigner()), - eContractid.HotWalletProxy, - [], - verify - ) as Promise; - -export const deployDelegationRegistry = async (verify?: boolean) => - withSaveAndVerify( - new DelegationRegistry__factory(await getFirstSigner()), - eContractid.DelegationRegistry, - [], - verify - ) as Promise; +//////////////////////////////////////////////////////////////////////////////// +// PLS ONLY APPEND MOCK CONTRACTS HERE! +//////////////////////////////////////////////////////////////////////////////// diff --git a/helpers/contracts-getters.ts b/helpers/contracts-getters.ts index 09de82804..7c48c7c88 100644 --- a/helpers/contracts-getters.ts +++ b/helpers/contracts-getters.ts @@ -90,6 +90,11 @@ import { HotWalletProxy__factory, NTokenOtherdeed__factory, DelegationRegistry__factory, + DepositContract__factory, + StakefishNFTManager__factory, + StakefishValidatorV1__factory, + StakefishValidatorFactory__factory, + NTokenStakefish__factory, } from "../types"; import { getEthersSigners, @@ -1263,3 +1268,58 @@ export const getDelegationRegistry = async (address?: tEthereumAddress) => ).address, await getFirstSigner() ); + +export const getStakefishValidatorFactory = async ( + address?: tEthereumAddress +) => + await StakefishValidatorFactory__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.StakefishValidatorFactory}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + +export const getStakefishNFTManager = async (address?: tEthereumAddress) => + await StakefishNFTManager__factory.connect( + address || + ( + await getDb().get(`${eContractid.SFVLDR}.${DRE.network.name}`).value() + ).address, + await getFirstSigner() + ); + +export const getStakefishValidator = async (address?: tEthereumAddress) => + await StakefishValidatorV1__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.StakefishValidator}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + +export const getDepositContract = async (address?: tEthereumAddress) => + await DepositContract__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.DepositContract}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + +export const getNTokenStakefish = async (address?: tEthereumAddress) => + await NTokenStakefish__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.NTokenStakefishImpl}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); diff --git a/helpers/hardhat-constants.ts b/helpers/hardhat-constants.ts index cf55ec8da..c55d5205c 100644 --- a/helpers/hardhat-constants.ts +++ b/helpers/hardhat-constants.ts @@ -119,7 +119,7 @@ export const MULTI_SEND_CHUNK_SIZE = parseInt( export const VERSION = version; export const COMMIT = git.short(); -export const COMPILER_OPTIMIZER_RUNS = 800; +export const COMPILER_OPTIMIZER_RUNS = 1; export const COMPILER_VERSION = "0.8.10"; export const PKG_DATA = { version: VERSION, diff --git a/helpers/init-helpers.ts b/helpers/init-helpers.ts index 85368dadb..9d8fff566 100644 --- a/helpers/init-helpers.ts +++ b/helpers/init-helpers.ts @@ -50,6 +50,7 @@ import { deployAutoYieldApe, deployReserveTimeLockStrategy, deployOtherdeedNTokenImpl, + deployStakefishNTokenImpl, } from "./contracts-deployments"; import {ZERO_ADDRESS} from "./constants"; @@ -138,6 +139,7 @@ export const initReservesByHelper = async ( let PsApeVariableDebtTokenImplementationAddress = ""; let nTokenBAKCImplementationAddress = ""; let nTokenOTHRImplementationAddress = ""; + let nTokenStakefishImplementationAddress = ""; if (genericPTokenImplAddress) { await insertContractAddressInDb( @@ -329,6 +331,7 @@ export const initReservesByHelper = async ( eContractid.NTokenBAYCImpl, eContractid.NTokenMAYCImpl, eContractid.NTokenBAKCImpl, + eContractid.NTokenStakefishImpl, ].includes(xTokenImpl) ) { xTokenType[symbol] = "nft"; @@ -579,6 +582,16 @@ export const initReservesByHelper = async ( ).address; xTokenToUse = nTokenOTHRImplementationAddress; + } else if (reserveSymbol == ERC721TokenContractId.SFVLDR) { + nTokenStakefishImplementationAddress = ( + await deployStakefishNTokenImpl( + pool.address, + delegationRegistryAddress, + verify + ) + ).address; + + xTokenToUse = nTokenStakefishImplementationAddress; } if (!xTokenToUse) { diff --git a/helpers/oracles-helpers.ts b/helpers/oracles-helpers.ts index 51c20e50a..8792e5b78 100644 --- a/helpers/oracles-helpers.ts +++ b/helpers/oracles-helpers.ts @@ -14,6 +14,7 @@ import { ERC721OracleWrapper, MockAggregator, PriceOracle, + StakefishNFTOracleWrapper, UniswapV3OracleWrapper, } from "../types"; import { @@ -24,6 +25,7 @@ import { deployBaseCurrencySynchronicityPriceAdapter, deployExchangeRateSynchronicityPriceAdapter, deployCTokenSynchronicityPriceAdapter, + deployStakefishNFTOracleWrapper, } from "./contracts-deployments"; import {getParaSpaceConfig, waitForTx} from "./misc-utils"; import { @@ -69,7 +71,8 @@ export const deployAllAggregators = async ( | ERC721OracleWrapper | CLExchangeRateSynchronicityPriceAdapter | CLBaseCurrencySynchronicityPriceAdapter - | CLCETHSynchronicityPriceAdapter; + | CLCETHSynchronicityPriceAdapter + | StakefishNFTOracleWrapper; } = {}; const addressesProvider = await getPoolAddressesProvider(); const paraSpaceConfig = getParaSpaceConfig(); @@ -104,6 +107,13 @@ export const deployAllAggregators = async ( ); continue; } + if (tokenSymbol === ERC721TokenContractId.SFVLDR) { + aggregators[tokenSymbol] = await deployStakefishNFTOracleWrapper( + tokens[tokenSymbol].address, + verify + ); + continue; + } if (tokenSymbol === ERC20TokenContractId.wstETH) { aggregators[tokenSymbol] = await deployCLwstETHSynchronicityPriceAdapter( aggregators[ERC20TokenContractId.stETH].address, diff --git a/helpers/types.ts b/helpers/types.ts index 28adbc6d5..913040102 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -59,6 +59,7 @@ export enum XTokenType { PYieldToken = 13, PTokenCApe = 14, NTokenOtherdeed = 15, + NTokenStakefish = 16, } export type ConstructorArgs = ( @@ -263,8 +264,13 @@ export enum eContractid { TimeLockImpl = "TimeLockImpl", DefaultTimeLockStrategy = "DefaultTimeLockStrategy", NTokenOtherdeedImpl = "NTokenOtherdeedImpl", + NTokenStakefishImpl = "NTokenStakefishImpl", HotWalletProxy = "HotWalletProxy", + SFVLDR = "SFVLDR", DelegationRegistry = "DelegationRegistry", + StakefishValidator = "StakefishValidator", + StakefishValidatorFactory = "StakefishValidatorFactory", + DepositContract = "DepositContract", } /* @@ -531,6 +537,7 @@ export enum ERC721TokenContractId { BAKC = "BAKC", SEWER = "SEWER", PPG = "PPG", + SFVLDR = "SFVLDR", } export enum NTokenContractId { @@ -544,6 +551,7 @@ export enum NTokenContractId { nSEWER = "nSEWER", nPPG = "nPPG", nOTHR = "nOTHR", + nSFVLDR = "nSFVLDR", } export enum PTokenContractId { @@ -750,6 +758,7 @@ export interface ICommonConfiguration { IncentivesController: tEthereumAddress; Oracle: IOracleConfig; HotWallet: tEthereumAddress | undefined; + StakefishManager: tEthereumAddress | undefined; DelegationRegistry: tEthereumAddress; } diff --git a/market-config/auctionStrategies.ts b/market-config/auctionStrategies.ts index 52897819b..59d6ad8a8 100644 --- a/market-config/auctionStrategies.ts +++ b/market-config/auctionStrategies.ts @@ -134,6 +134,16 @@ export const auctionStrategyPudgyPenguins: IAuctionStrategyParams = { tickLength: "900", }; +export const auctionStrategyStakefishValidator: IAuctionStrategyParams = { + name: "auctionStrategyStakefishValidator", + maxPriceMultiplier: utils.parseUnits("2.6", 18).toString(), + minExpPriceMultiplier: utils.parseUnits("1.2", 18).toString(), + minPriceMultiplier: utils.parseUnits("0.8", 18).toString(), + stepLinear: utils.parseUnits("0.025", 18).toString(), + stepExp: utils.parseUnits("0.0483", 18).toString(), + tickLength: "900", +}; + //////////////////////////////////////////////////////////// // MOCK //////////////////////////////////////////////////////////// diff --git a/market-config/index.ts b/market-config/index.ts index 78daa9945..bd4ab8414 100644 --- a/market-config/index.ts +++ b/market-config/index.ts @@ -47,6 +47,7 @@ import { strategyRETH, strategyBENDETH, strategyFRAX, + strategyStakefishValidator, } from "./reservesConfigs"; export const CommonConfig: Pick< @@ -69,6 +70,7 @@ export const CommonConfig: Pick< | "Mocks" | "Oracle" | "HotWallet" + | "StakefishManager" > = { WrappedNativeTokenId: ERC20TokenContractId.WETH, MarketId: "ParaSpaceMM", @@ -91,6 +93,7 @@ export const CommonConfig: Pick< // Oracle Oracle: TestnetOracleConfig, HotWallet: undefined, + StakefishManager: undefined, }; export const HardhatParaSpaceConfig: IParaSpaceConfiguration = { @@ -137,6 +140,7 @@ export const HardhatParaSpaceConfig: IParaSpaceConfiguration = { BAKC: strategyBAKC, SEWER: strategySEWER, PPG: strategyPudgyPenguins, + SFVLDR: strategyStakefishValidator, }, DelegationRegistry: ZERO_ADDRESS, }; @@ -196,6 +200,7 @@ export const GoerliParaSpaceConfig: IParaSpaceConfiguration = { UniswapV3: "0xC36442b4a4522E871399CD717aBDD847Ab11FE88", PPG: "0xf140558cA4d4e10f63661504D4F3f74FADDDe3E8", SEWER: "0x3aa026cd539fa1f6ae58ae238a10e2f1cf831454", + SFVLDR: "0x5b41ffb9c448c02ff3d0401b0374b67efcb73c7e", }, YogaLabs: { ApeCoinStaking: "0xeF37717B1807a253c6D140Aca0141404D23c26D4", @@ -256,7 +261,9 @@ export const GoerliParaSpaceConfig: IParaSpaceConfiguration = { BAKC: strategyBAKC, SEWER: strategySEWER, PPG: strategyPudgyPenguins, + SFVLDR: strategyStakefishValidator, }, + StakefishManager: "0x5b41ffb9c448c02ff3d0401b0374b67efcb73c7e", DelegationRegistry: "0x00000000000076A84feF008CDAbe6409d2FE638B", }; @@ -312,6 +319,7 @@ export const MainnetParaSpaceConfig: IParaSpaceConfiguration = { cETH: "0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5", SEWER: "0x764AeebcF425d56800eF2c84F2578689415a2DAa", PPG: "0xbd3531da5cf5857e7cfaa92426877b022e612cf8", + SFVLDR: "0xffff2d93c83d4c613ed68ca887f057651135e089", }, YogaLabs: { ApeCoinStaking: "0x5954aB967Bc958940b7EB73ee84797Dc8a2AFbb9", @@ -383,10 +391,12 @@ export const MainnetParaSpaceConfig: IParaSpaceConfiguration = { BAKC: strategyBAKC, SEWER: strategySEWER, PPG: strategyPudgyPenguins, + SFVLDR: strategyStakefishValidator, }, Mocks: undefined, Oracle: MainnetOracleConfig, HotWallet: "0xC3AA9bc72Bd623168860a1e5c6a4530d3D80456c", + StakefishManager: "0xffff2d93c83d4c613ed68ca887f057651135e089", DelegationRegistry: "0x00000000000076A84feF008CDAbe6409d2FE638B", }; diff --git a/market-config/reservesConfigs.ts b/market-config/reservesConfigs.ts index 3b07c2388..07a00c016 100644 --- a/market-config/reservesConfigs.ts +++ b/market-config/reservesConfigs.ts @@ -11,6 +11,7 @@ import { auctionStrategyOthr, auctionStrategyPudgyPenguins, auctionStrategySEWER, + auctionStrategyStakefishValidator, auctionStrategyUniswapV3, auctionStrategyWPunks, auctionStrategyZero, @@ -54,6 +55,7 @@ import { timeLockStrategyRETH, timeLockStrategySAPE, timeLockStrategySEWER, + timeLockStrategyStakefishValidator, timeLockStrategyUniswapV3, timeLockStrategyUSDC, timeLockStrategyUSDT, @@ -469,6 +471,22 @@ export const strategyPudgyPenguins: IReserveParams = { supplyCap: "0", }; +export const strategyStakefishValidator: IReserveParams = { + strategy: rateStrategyNFT, + auctionStrategy: auctionStrategyStakefishValidator, + timeLockStrategy: timeLockStrategyStakefishValidator, + baseLTVAsCollateral: "3000", + liquidationProtocolFeePercentage: "0", + liquidationThreshold: "6500", + liquidationBonus: "10500", + borrowingEnabled: false, + reserveDecimals: "0", + xTokenImpl: eContractid.NTokenStakefishImpl, + reserveFactor: "0", + borrowCap: "0", + supplyCap: "0", +}; + //////////////////////////////////////////////////////////// // V2 //////////////////////////////////////////////////////////// diff --git a/market-config/timeLockStrategies.ts b/market-config/timeLockStrategies.ts index a7349489f..2f83b02ec 100644 --- a/market-config/timeLockStrategies.ts +++ b/market-config/timeLockStrategies.ts @@ -432,3 +432,15 @@ export const timeLockStrategyPenguins: ITimeLockStrategyParams = { poolPeriodLimit: "2", period: "86400", }; + +export const timeLockStrategyStakefishValidator: ITimeLockStrategyParams = { + name: "timeLockStrategyStakefishValidator", + minThreshold: "4", + midThreshold: "12", + minWaitTime: "12", + midWaitTime: "7200", + maxWaitTime: "43200", + poolPeriodWaitTime: "600", + poolPeriodLimit: "2", + period: "86400", +}; diff --git a/scripts/deployments/steps/11_allReserves.ts b/scripts/deployments/steps/11_allReserves.ts index ed6edebd5..64242d8b4 100644 --- a/scripts/deployments/steps/11_allReserves.ts +++ b/scripts/deployments/steps/11_allReserves.ts @@ -101,7 +101,8 @@ export const step_11 = async (verify = false) => { xTokenImpl === eContractid.PTokenSApeImpl || xTokenImpl === eContractid.PTokenCApeImpl || xTokenImpl === eContractid.PYieldTokenImpl || - xTokenImpl === eContractid.NTokenBAKCImpl + xTokenImpl === eContractid.NTokenBAKCImpl || + xTokenImpl === eContractid.NTokenStakefishImpl ) as [string, IReserveParams][]; const chunkedReserves = chunk(reserves, 20); diff --git a/scripts/deployments/steps/22_timelock.ts b/scripts/deployments/steps/22_timelock.ts index f5a3d11a9..fdacbc700 100644 --- a/scripts/deployments/steps/22_timelock.ts +++ b/scripts/deployments/steps/22_timelock.ts @@ -1,5 +1,6 @@ import {deployTimeLockImplAndAssignItToProxy} from "../../../helpers/contracts-deployments"; import {getPoolAddressesProvider} from "../../../helpers/contracts-getters"; + export const step_22 = async (verify = false) => { try { const addressesProvider = await getPoolAddressesProvider(); diff --git a/scripts/dev/12.set-timelock-strategy.ts b/scripts/dev/12.set-timelock-strategy.ts index 3bb8cf2ef..63a29a9e9 100644 --- a/scripts/dev/12.set-timelock-strategy.ts +++ b/scripts/dev/12.set-timelock-strategy.ts @@ -46,6 +46,7 @@ import { timeLockStrategySEWER, timeLockStrategyPenguins, timeLockStrategySTETH, + timeLockStrategyStakefishValidator, } from "../../market-config/timeLockStrategies"; const TIME_LOCK_STRATEGY = { @@ -99,6 +100,7 @@ const TIME_LOCK_STRATEGY = { SEWER: timeLockStrategySEWER, SewerPass: timeLockStrategySEWER, PPG: timeLockStrategyPenguins, + "SF-STAKE-VLDR": timeLockStrategyStakefishValidator, }; const setTimeLockStrategy = async () => { @@ -109,6 +111,8 @@ const setTimeLockStrategy = async () => { const configurator = await getPoolConfiguratorProxy(); const reservesData = await ui.getAllReservesTokens(); for (const x of reservesData) { + if (x.tokenAddress != "0x5B41FFB9C448C02Ff3D0401b0374b67EFcB73C7E") + continue; const strategy = TIME_LOCK_STRATEGY[x.symbol]; if (!strategy) { console.log("no stratey found for", x.symbol); diff --git a/scripts/upgrade/ntoken.ts b/scripts/upgrade/ntoken.ts index 3e613319e..f757a078a 100644 --- a/scripts/upgrade/ntoken.ts +++ b/scripts/upgrade/ntoken.ts @@ -6,6 +6,7 @@ import { deployNTokenBAYCImpl, deployNTokenMAYCImpl, deployOtherdeedNTokenImpl, + deployStakefishNTokenImpl, deployUniswapV3NTokenImpl, } from "../../helpers/contracts-deployments"; import { @@ -42,6 +43,7 @@ export const upgradeNToken = async (verify = false) => { let nTokenMoonBirdImplementationAddress = ""; let nTokenUniSwapV3ImplementationAddress = ""; let nTokenOTHRImplementationAddress = ""; + let nTokenStakefishImplementationAddress = ""; let newImpl = ""; for (let i = 0; i < allXTokens.length; i++) { @@ -63,6 +65,7 @@ export const upgradeNToken = async (verify = false) => { XTokenType.NTokenMAYC, XTokenType.NTokenBAKC, XTokenType.NTokenOtherdeed, + XTokenType.NTokenStakefish, ].includes(xTokenType) ) { continue; @@ -159,6 +162,18 @@ export const upgradeNToken = async (verify = false) => { ).address; } newImpl = nTokenOTHRImplementationAddress; + } else if (xTokenType == XTokenType.NTokenStakefish) { + if (!nTokenStakefishImplementationAddress) { + console.log("deploy NTokenStakefish implementation"); + nTokenStakefishImplementationAddress = ( + await deployStakefishNTokenImpl( + poolAddress, + delegationRegistry, + verify + ) + ).address; + } + newImpl = nTokenStakefishImplementationAddress; } else if (xTokenType == XTokenType.NToken) { // compatibility if (symbol == NTokenContractId.nOTHR) { diff --git a/scripts/upgrade/pool.ts b/scripts/upgrade/pool.ts index af5b68f64..779c37db3 100644 --- a/scripts/upgrade/pool.ts +++ b/scripts/upgrade/pool.ts @@ -81,6 +81,7 @@ const resetSelectors = async () => { for (const facet of facets.filter( (x) => x.implAddress !== "0x0874eBaad20aE4a6F1623a3bf6f914355B7258dB" && + x.implAddress !== "0x0b6717ED22Cfd5495E47804F3f6624E5f0Ea20Cb" && x.implAddress !== "0xC85d346eB17B37b93B30a37603Ef9550Ab18aC83" // ParaProxyInterfaces )) { implementations.push({ diff --git a/tasks/deployments/23_renounceOwnership.ts b/tasks/deployments/23_renounceOwnership.ts index 50db7411e..7d659c537 100644 --- a/tasks/deployments/23_renounceOwnership.ts +++ b/tasks/deployments/23_renounceOwnership.ts @@ -5,7 +5,7 @@ task("deploy:renounce-ownership", "Renounce deployer ownership") .addPositionalParam("newAdmin", "New Admin Address") .setAction(async ({newAdmin}, DRE) => { await DRE.run("set-DRE"); - const {step_23} = await import( + const {step_23: step_23} = await import( "../../scripts/deployments/steps/23_renounceOwnership" ); await step_23(ETHERSCAN_VERIFICATION, { diff --git a/test/_stakefish_nft.spec.ts b/test/_stakefish_nft.spec.ts new file mode 100644 index 000000000..fd55503c8 --- /dev/null +++ b/test/_stakefish_nft.spec.ts @@ -0,0 +1,362 @@ +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {expect} from "chai"; +import {parseEther} from "ethers/lib/utils"; +import {ZERO_ADDRESS} from "../helpers/constants"; +import {getStakefishValidator} from "../helpers/contracts-getters"; +import { + convertToCurrencyDecimals, + getCurrentTime, +} from "../helpers/contracts-helpers"; +import {DRE, waitForTx} from "../helpers/misc-utils"; +import {StakefishValidatorV1} from "../types"; +import {SignerWithAddress} from "./helpers/make-suite"; +import {testEnvFixture} from "./helpers/setup-env"; +import {supplyAndValidate} from "./helpers/validated-steps"; + +describe("Stakefish NFT", () => { + let user1: SignerWithAddress; + let user2: SignerWithAddress; + let user3: SignerWithAddress; + let user4: SignerWithAddress; + let user5: SignerWithAddress; + let user6: SignerWithAddress; + let validator2: StakefishValidatorV1; + let validator3: StakefishValidatorV1; + let validator4: StakefishValidatorV1; + let validator5: StakefishValidatorV1; + + const pubkey = + "0x877d383705a1514c38060f2de4365b9b0a05c0de9aa5813f4effd412a9fa896ed938d761c2d0fef9422bb3992a01b4b7"; + const signature = + "0xa3aae3013474be42182ecdc8f2ff4c0082cfb2b81dfbc93e0f0210d805ece887a19a841762cd1efb65044dc810dc371618a052d18c15280bfa85b5f66dde9c81466f4a552e727dfcfd70d1012c63df07c8dba0b3891a39b6ce6c179fb454d122"; + const depositDataRoot = + "0xc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6"; + + const fixture = async () => { + const testEnv = await loadFixture(testEnvFixture); + + user1 = testEnv.users[0]; + user2 = testEnv.users[1]; + user3 = testEnv.users[2]; + user4 = testEnv.users[3]; + user5 = testEnv.users[4]; + user6 = testEnv.users[5]; + + // PreDeposit + await waitForTx( + await testEnv.sfvldr + .connect(user1.signer) + .mint(1, {value: parseEther("32")}) + ); + + // PostDeposit + await waitForTx( + await testEnv.sfvldr + .connect(user2.signer) + .mint(1, {value: parseEther("32")}) + ); + validator2 = await getStakefishValidator( + await testEnv.sfvldr.validatorForTokenId("2") + ); + await waitForTx( + await validator2 + .connect(testEnv.poolAdmin.signer) + .makeEth2Deposit(pubkey, signature, depositDataRoot) + ); + + // Active + await waitForTx( + await testEnv.sfvldr + .connect(user3.signer) + .mint(1, {value: parseEther("32")}) + ); + validator3 = await getStakefishValidator( + await testEnv.sfvldr.validatorForTokenId("3") + ); + await waitForTx( + await validator3 + .connect(testEnv.poolAdmin.signer) + .makeEth2Deposit(pubkey, signature, depositDataRoot) + ); + await waitForTx( + await validator3 + .connect(testEnv.poolAdmin.signer) + .validatorStarted(await getCurrentTime(), "1", ZERO_ADDRESS) + ); + + // Exited + await waitForTx( + await testEnv.sfvldr + .connect(user4.signer) + .mint(1, {value: parseEther("32")}) + ); + validator4 = await getStakefishValidator( + await testEnv.sfvldr.validatorForTokenId("4") + ); + await waitForTx( + await validator4 + .connect(testEnv.poolAdmin.signer) + .makeEth2Deposit(pubkey, signature, depositDataRoot) + ); + await waitForTx( + await validator4 + .connect(testEnv.poolAdmin.signer) + .validatorStarted(await getCurrentTime(), "2", ZERO_ADDRESS) + ); + await waitForTx(await validator4.connect(user4.signer).requestExit()); + await waitForTx( + await validator4 + .connect(testEnv.poolAdmin.signer) + .validatorExited(await getCurrentTime()) + ); + + // Burnable + await waitForTx( + await testEnv.sfvldr + .connect(user5.signer) + .mint(1, {value: parseEther("32")}) + ); + validator5 = await getStakefishValidator( + await testEnv.sfvldr.validatorForTokenId("5") + ); + await waitForTx(await validator5.connect(user5.signer).withdraw()); + + await waitForTx( + await testEnv.sfvldr + .connect(user1.signer) + .setApprovalForAll(testEnv.pool.address, true) + ); + await waitForTx( + await testEnv.sfvldr + .connect(user2.signer) + .setApprovalForAll(testEnv.pool.address, true) + ); + await waitForTx( + await testEnv.sfvldr + .connect(user3.signer) + .setApprovalForAll(testEnv.pool.address, true) + ); + await waitForTx( + await testEnv.sfvldr + .connect(user4.signer) + .setApprovalForAll(testEnv.pool.address, true) + ); + await waitForTx( + await testEnv.sfvldr + .connect(user5.signer) + .setApprovalForAll(testEnv.pool.address, true) + ); + + await supplyAndValidate(testEnv.usdc, "100000", user6, true); + + return testEnv; + }; + + it("TC-stakefish-nft-01: nft price is correct when nft state is PreDeposit", async () => { + const {paraspaceOracle, sfvldr} = await loadFixture(fixture); + expect(await paraspaceOracle.getTokenPrice(sfvldr.address, "1")).eq( + parseEther("32") + ); + }); + + it("TC-stakefish-nft-02: nft price is correct when nft state is PostDeposit", async () => { + const {paraspaceOracle, sfvldr} = await loadFixture(fixture); + expect(await paraspaceOracle.getTokenPrice(sfvldr.address, "2")).eq( + parseEther("32") + ); + }); + + it("TC-stakefish-nft-03: nft price is correct when nft state is Active", async () => { + const {paraspaceOracle, sfvldr} = await loadFixture(fixture); + expect(await paraspaceOracle.getTokenPrice(sfvldr.address, "3")).eq( + parseEther("32") + ); + + // simulate rewards + await user6.signer.sendTransaction({ + to: validator3.address, + value: parseEther("1"), + }); + expect(await paraspaceOracle.getTokenPrice(sfvldr.address, "3")).eq( + parseEther("33") + ); + + // withdraw rewards + await waitForTx(await validator3.connect(user3.signer).withdraw()); + expect(await DRE.ethers.provider.getBalance(validator3.address)).eq(0); + expect(await paraspaceOracle.getTokenPrice(sfvldr.address, "3")).eq( + parseEther("32") + ); + }); + + it("TC-stakefish-nft-04: nft price is correct when nft state is Exited", async () => { + const {paraspaceOracle, sfvldr} = await loadFixture(fixture); + expect(await paraspaceOracle.getTokenPrice(sfvldr.address, "4")).eq( + parseEther("32") + ); + }); + + it("TC-stakefish-nft-05: nft price is correct when nft state is Burnable", async () => { + const {paraspaceOracle, sfvldr} = await loadFixture(fixture); + expect(await paraspaceOracle.getTokenPrice(sfvldr.address, "5")).eq( + parseEther("0") + ); + }); + + it("TC-stakefish-nft-07: Burnable nft cannot be supplied", async () => { + const {paraspaceOracle, sfvldr, pool} = await loadFixture(fixture); + expect(await paraspaceOracle.getTokenPrice(sfvldr.address, "5")).eq( + parseEther("0") + ); + + await expect( + pool.connect(user5.signer).supplyERC721( + sfvldr.address, + [ + { + tokenId: "5", + useAsCollateral: true, + }, + ], + user5.address, + 0, + {gasLimit: 5000000} + ) + ).to.be.reverted; + }); + + it("TC-stakefish-nft-08: Rewards can be claimed via pool", async () => { + const {paraspaceOracle, sfvldr, pool} = await loadFixture(fixture); + + await waitForTx( + await pool.connect(user3.signer).supplyERC721( + sfvldr.address, + [ + { + tokenId: "3", + useAsCollateral: true, + }, + ], + user3.address, + 0, + {gasLimit: 5000000} + ) + ); + + // simulate rewards + await user6.signer.sendTransaction({ + to: validator3.address, + value: parseEther("1"), + }); + expect(await paraspaceOracle.getTokenPrice(sfvldr.address, "3")).eq( + parseEther("33") + ); + + const beforeBalance = await DRE.ethers.provider.getBalance(user6.address); + await waitForTx( + await pool + .connect(user3.signer) + .claimStakefishWithdrawals(sfvldr.address, ["3"], user6.address) + ); + const afterBalance = await DRE.ethers.provider.getBalance(user6.address); + + expect(afterBalance.sub(beforeBalance)).eq(parseEther("1")); + }); + + it("TC-stakefish-nft-09: Unable to claim full withdraw if there is borrow", async () => { + const {sfvldr, pool, usdc} = await loadFixture(fixture); + + await waitForTx( + await pool.connect(user3.signer).supplyERC721( + sfvldr.address, + [ + { + tokenId: "3", + useAsCollateral: true, + }, + ], + user3.address, + 0, + {gasLimit: 5000000} + ) + ); + + await waitForTx( + await pool + .connect(user3.signer) + .borrow( + usdc.address, + await convertToCurrencyDecimals(usdc.address, "10000"), + 0, + user3.address, + {gasLimit: 5000000} + ) + ); + + // simulate full withdraw + await user6.signer.sendTransaction({ + to: validator3.address, + value: parseEther("33"), + }); + + await expect( + pool + .connect(user3.signer) + .claimStakefishWithdrawals(sfvldr.address, ["3"], user6.address) + ).to.be.reverted; + }); + + it("TC-stakefish-nft-10: not owner error will be thrown if someone tries to claimWithdrawals of others", async () => { + const {sfvldr, pool} = await loadFixture(fixture); + + await waitForTx( + await pool.connect(user3.signer).supplyERC721( + sfvldr.address, + [ + { + tokenId: "3", + useAsCollateral: true, + }, + ], + user3.address, + 0, + {gasLimit: 5000000} + ) + ); + + // simulate rewards + await user6.signer.sendTransaction({ + to: validator3.address, + value: parseEther("1"), + }); + + await expect( + pool + .connect(user4.signer) + .claimStakefishWithdrawals(sfvldr.address, ["3"], user6.address) + ).to.be.reverted; + }); + + it("TC-stakefish-nft-11: nft owner can requestExit via nToken", async () => { + const {sfvldr, pool, nSfvldr} = await loadFixture(fixture); + + await waitForTx( + await pool.connect(user3.signer).supplyERC721( + sfvldr.address, + [ + { + tokenId: "3", + useAsCollateral: true, + }, + ], + user3.address, + 0, + {gasLimit: 5000000} + ) + ); + + await waitForTx(await nSfvldr.connect(user3.signer).requestExit(["3"])); + + expect((await validator3.lastStateChange()).state).eq(3); + }); +}); diff --git a/test/helpers/make-suite.ts b/test/helpers/make-suite.ts index 4c132a108..6f6aa9f65 100644 --- a/test/helpers/make-suite.ts +++ b/test/helpers/make-suite.ts @@ -53,6 +53,8 @@ import { getWstETH, getMockCToken, getNTokenOtherdeed, + getStakefishNFTManager, + getNTokenStakefish, } from "../../helpers/contracts-getters"; import { eContractid, @@ -79,10 +81,12 @@ import { NTokenMAYC, NTokenMoonBirds, NTokenOtherdeed, + NTokenStakefish, NTokenUniswapV3, PausableZone, PausableZoneController, SeaportAdapter, + StakefishNFTManager, StETHDebtToken, UiPoolDataProvider, WstETHMocked, @@ -168,6 +172,8 @@ export interface TestEnv { nBAYC: NTokenBAYC; nOTHR: NTokenOtherdeed; bayc: MintableERC721; + sfvldr: StakefishNFTManager; + nSfvldr: NTokenStakefish; addressesProvider: PoolAddressesProvider; registry: PoolAddressesProviderRegistry; aclManager: ACLManager; @@ -246,6 +252,8 @@ export async function initializeMakeSuite() { nMOONBIRD: {} as NTokenMoonBirds, nBAKC: {} as NTokenBAKC, bayc: {} as MintableERC721, + sfvldr: {} as StakefishNFTManager, + nSfvldr: {} as NTokenStakefish, addressesProvider: {} as PoolAddressesProvider, registry: {} as PoolAddressesProviderRegistry, aclManager: {} as ACLManager, @@ -390,6 +398,10 @@ export async function initializeMakeSuite() { (xToken) => xToken.symbol === NTokenContractId.nBAKC )?.tokenAddress; + const nSfvldrAddress = allTokens.find( + (xToken) => xToken.symbol === NTokenContractId.nSFVLDR + )?.tokenAddress; + const nWPunkAddress = allTokens.find( (xToken) => xToken.symbol === NTokenContractId.nWPUNKS )?.tokenAddress; @@ -442,6 +454,9 @@ export async function initializeMakeSuite() { const baycAddress = reservesTokens.find( (token) => token.symbol === ERC721TokenContractId.BAYC )?.tokenAddress; + const sfvldrAddress = reservesTokens.find( + (token) => token.symbol === ERC721TokenContractId.SFVLDR + )?.tokenAddress; const punksAddress = reservesTokens.find( (token) => token.symbol === eContractid.PUNKS )?.tokenAddress; @@ -510,6 +525,7 @@ export async function initializeMakeSuite() { testEnv.nMAYC = await getNTokenMAYC(nMAYCAddress); testEnv.nDOODLE = await getNToken(nDOODLEAddress); testEnv.nBAKC = await getNTokenBAKC(nBAKCAddress); + testEnv.nSfvldr = await getNTokenStakefish(nSfvldrAddress); testEnv.nMOONBIRD = await getNTokenMoonBirds(nMOONBIRDAddress); @@ -522,6 +538,8 @@ export async function initializeMakeSuite() { testEnv.bayc = await getMintableERC721(baycAddress); + testEnv.sfvldr = await getStakefishNFTManager(sfvldrAddress); + testEnv.nWPunk = await getNToken(nWPunkAddress); testEnv.punks = await getPunks(punksAddress); testEnv.wPunk = await getWPunk(wpunkAddress);